Merge branch 'master' into christina/course-settings-drupal
This commit is contained in:
3
AUTHORS
3
AUTHORS
@@ -75,4 +75,5 @@ Frances Botsford <frances@edx.org>
|
||||
Jonah Stanley <Jonah_Stanley@brown.edu>
|
||||
Slater Victoroff <slater.r.victoroff@gmail.com>
|
||||
Peter Fogg <peter.p.fogg@gmail.com>
|
||||
Renzo Lucioni <renzolucioni@gmail.com>
|
||||
Bethany LaPenta <lapentab@mit.edu>
|
||||
Renzo Lucioni <renzolucioni@gmail.com>
|
||||
|
||||
@@ -5,6 +5,28 @@ These are notable changes in edx-platform. This is a rolling list of changes,
|
||||
in roughly chronological order, most recent first. Add your entries at or near
|
||||
the top. Include a label indicating the component affected.
|
||||
|
||||
XModule: Only write out assets files if the contents have changed.
|
||||
|
||||
XModule: Don't delete generated xmodule asset files when compiling (for
|
||||
instance, when XModule provides a coffeescript file, don't delete
|
||||
the associated javascript)
|
||||
|
||||
Common: Make asset watchers run as singletons (so they won't start if the
|
||||
watcher is already running in another shell).
|
||||
|
||||
Common: Use coffee directly when watching for coffeescript file changes.
|
||||
|
||||
Common: Make rake provide better error messages if packages are missing.
|
||||
|
||||
Common: Repairs development documentation generation by sphinx.
|
||||
|
||||
LMS: Problem rescoring. Added options on the Grades tab of the
|
||||
Instructor Dashboard to allow all students' submissions for a
|
||||
particular problem to be rescored. Also supports resetting all
|
||||
students' number of attempts to zero. Provides a list of background
|
||||
tasks that are currently running for the course, and an option to
|
||||
see a history of background tasks for a given problem.
|
||||
|
||||
LMS: Forums. Added handling for case where discussion module can get `None` as
|
||||
value of lms.start in `lms/djangoapps/django_comment_client/utils.py`
|
||||
|
||||
|
||||
1
Gemfile
1
Gemfile
@@ -4,3 +4,4 @@ gem 'sass', '3.1.15'
|
||||
gem 'bourbon', '~> 1.3.6'
|
||||
gem 'colorize', '~> 0.5.8'
|
||||
gem 'launchy', '~> 2.1.2'
|
||||
gem 'sys-proctable', '~> 0.9.3'
|
||||
|
||||
@@ -39,8 +39,6 @@ def get_users_in_course_group_by_role(location, role):
|
||||
'''
|
||||
Create all permission groups for a new course and subscribe the caller into those roles
|
||||
'''
|
||||
|
||||
|
||||
def create_all_course_groups(creator, location):
|
||||
create_new_course_group(creator, location, INSTRUCTOR_ROLE_NAME)
|
||||
create_new_course_group(creator, location, STAFF_ROLE_NAME)
|
||||
@@ -57,13 +55,11 @@ def create_new_course_group(creator, location, role):
|
||||
|
||||
return
|
||||
|
||||
'''
|
||||
This is to be called only by either a command line code path or through a app which has already
|
||||
asserted permissions
|
||||
'''
|
||||
|
||||
|
||||
def _delete_course_group(location):
|
||||
'''
|
||||
This is to be called only by either a command line code path or through a app which has already
|
||||
asserted permissions
|
||||
'''
|
||||
# remove all memberships
|
||||
instructors = Group.objects.get(name=get_course_groupname_for_role(location, INSTRUCTOR_ROLE_NAME))
|
||||
for user in instructors.user_set.all():
|
||||
@@ -75,13 +71,11 @@ def _delete_course_group(location):
|
||||
user.groups.remove(staff)
|
||||
user.save()
|
||||
|
||||
'''
|
||||
This is to be called only by either a command line code path or through an app which has already
|
||||
asserted permissions to do this action
|
||||
'''
|
||||
|
||||
|
||||
def _copy_course_group(source, dest):
|
||||
'''
|
||||
This is to be called only by either a command line code path or through an app which has already
|
||||
asserted permissions to do this action
|
||||
'''
|
||||
instructors = Group.objects.get(name=get_course_groupname_for_role(source, INSTRUCTOR_ROLE_NAME))
|
||||
new_instructors_group = Group.objects.get(name=get_course_groupname_for_role(dest, INSTRUCTOR_ROLE_NAME))
|
||||
for user in instructors.user_set.all():
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
#pylint: disable=W0621
|
||||
|
||||
from lettuce import world, step
|
||||
from nose.tools import assert_false, assert_equal, assert_regexp_matches
|
||||
from nose.tools import assert_false, assert_equal, assert_regexp_matches, assert_true
|
||||
from common import type_in_codemirror
|
||||
|
||||
KEY_CSS = '.key input.policy-key'
|
||||
@@ -28,7 +28,14 @@ def i_am_on_advanced_course_settings(step):
|
||||
@step(u'I press the "([^"]*)" notification button$')
|
||||
def press_the_notification_button(step, name):
|
||||
css = 'a.%s-button' % name.lower()
|
||||
world.css_click(css)
|
||||
|
||||
# Save was clicked if either the save notification bar is gone, or we have a error notification
|
||||
# overlaying it (expected in the case of typing Object into display_name).
|
||||
save_clicked = lambda : world.is_css_not_present('.is-shown.wrapper-notification-warning') or \
|
||||
world.is_css_present('.is-shown.wrapper-notification-error')
|
||||
|
||||
assert_true(world.css_click(css, success_condition=save_clicked),
|
||||
'The save button was not clicked after 5 attempts.')
|
||||
|
||||
|
||||
@step(u'I edit the value of a policy key$')
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#pylint: disable=C0111
|
||||
#pylint: disable=W0621
|
||||
# pylint: disable=C0111
|
||||
# pylint: disable=W0621
|
||||
|
||||
from lettuce import world, step
|
||||
from nose.tools import assert_true
|
||||
@@ -16,7 +16,7 @@ logger = getLogger(__name__)
|
||||
########### STEP HELPERS ##############
|
||||
|
||||
@step('I (?:visit|access|open) the Studio homepage$')
|
||||
def i_visit_the_studio_homepage(step):
|
||||
def i_visit_the_studio_homepage(_step):
|
||||
# To make this go to port 8001, put
|
||||
# LETTUCE_SERVER_PORT = 8001
|
||||
# in your settings.py file.
|
||||
@@ -26,17 +26,17 @@ def i_visit_the_studio_homepage(step):
|
||||
|
||||
|
||||
@step('I am logged into Studio$')
|
||||
def i_am_logged_into_studio(step):
|
||||
def i_am_logged_into_studio(_step):
|
||||
log_into_studio()
|
||||
|
||||
|
||||
@step('I confirm the alert$')
|
||||
def i_confirm_with_ok(step):
|
||||
def i_confirm_with_ok(_step):
|
||||
world.browser.get_alert().accept()
|
||||
|
||||
|
||||
@step(u'I press the "([^"]*)" delete icon$')
|
||||
def i_press_the_category_delete_icon(step, category):
|
||||
def i_press_the_category_delete_icon(_step, category):
|
||||
if category == 'section':
|
||||
css = 'a.delete-button.delete-section-button span.delete-icon'
|
||||
elif category == 'subsection':
|
||||
@@ -47,7 +47,7 @@ def i_press_the_category_delete_icon(step, category):
|
||||
|
||||
|
||||
@step('I have opened a new course in Studio$')
|
||||
def i_have_opened_a_new_course(step):
|
||||
def i_have_opened_a_new_course(_step):
|
||||
open_new_course()
|
||||
|
||||
|
||||
@@ -73,7 +73,6 @@ def create_studio_user(
|
||||
registration.register(studio_user)
|
||||
registration.activate()
|
||||
|
||||
|
||||
def fill_in_course_info(
|
||||
name='Robot Super Course',
|
||||
org='MITx',
|
||||
@@ -107,7 +106,7 @@ def log_into_studio(
|
||||
|
||||
|
||||
def create_a_course():
|
||||
c = world.CourseFactory.create(org='MITx', course='999', display_name='Robot Super Course')
|
||||
world.CourseFactory.create(org='MITx', course='999', display_name='Robot Super Course')
|
||||
|
||||
# Add the user to the instructor group of the course
|
||||
# so they will have the permissions to see it in studio
|
||||
@@ -147,6 +146,7 @@ def set_date_and_time(date_css, desired_date, time_css, desired_time):
|
||||
world.css_fill(date_css, desired_date)
|
||||
# hit TAB to get to the time field
|
||||
e = world.css_find(date_css).first
|
||||
# pylint: disable=W0212
|
||||
e._element.send_keys(Keys.TAB)
|
||||
world.css_fill(time_css, desired_time)
|
||||
e = world.css_find(time_css).first
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#pylint: disable=C0111
|
||||
#pylint: disable=W0621
|
||||
# pylint: disable=C0111
|
||||
# pylint: disable=W0621
|
||||
|
||||
from lettuce import world, step
|
||||
from common import *
|
||||
@@ -8,7 +8,7 @@ from nose.tools import assert_equal
|
||||
############### ACTIONS ####################
|
||||
|
||||
|
||||
@step('I click the new section link$')
|
||||
@step('I click the New Section link$')
|
||||
def i_click_new_section_link(_step):
|
||||
link_css = 'a.new-courseware-section-button'
|
||||
world.css_click(link_css)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#pylint: disable=C0111
|
||||
#pylint: disable=W0621
|
||||
# pylint: disable=C0111
|
||||
# pylint: disable=W0621
|
||||
|
||||
from lettuce import world, step
|
||||
from common import *
|
||||
|
||||
@@ -6,13 +6,13 @@ from lettuce import world, step
|
||||
|
||||
|
||||
@step('when I view the video it does not have autoplay enabled')
|
||||
def does_not_autoplay(step):
|
||||
def does_not_autoplay(_step):
|
||||
assert world.css_find('.video')[0]['data-autoplay'] == 'False'
|
||||
assert world.css_find('.video_control')[0].has_class('play')
|
||||
|
||||
|
||||
@step('creating a video takes a single click')
|
||||
def video_takes_a_single_click(step):
|
||||
def video_takes_a_single_click(_step):
|
||||
assert(not world.is_css_present('.xmodule_VideoModule'))
|
||||
world.css_click("a[data-location='i4x://edx/templates/video/default']")
|
||||
assert(world.is_css_present('.xmodule_VideoModule'))
|
||||
|
||||
@@ -39,10 +39,7 @@ def get_module_info(store, location, parent_location=None, rewrite_static_links=
|
||||
def set_module_info(store, location, post_data):
|
||||
module = None
|
||||
try:
|
||||
if location.revision is None:
|
||||
module = store.get_item(location)
|
||||
else:
|
||||
module = store.get_item(location)
|
||||
module = store.get_item(location)
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
@@ -99,6 +99,7 @@ class ChecklistTestCase(CourseTestCase):
|
||||
'name': self.course.location.name,
|
||||
'checklist_index': 2})
|
||||
|
||||
|
||||
def get_first_item(checklist):
|
||||
return checklist['items'][0]
|
||||
|
||||
|
||||
@@ -132,7 +132,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
|
||||
# just pick one vertical
|
||||
descriptor = store.get_items(Location('i4x', 'edX', 'simple', 'vertical', None, None))[0]
|
||||
location = descriptor.location._replace(name='.' + descriptor.location.name)
|
||||
location = descriptor.location.replace(name='.' + descriptor.location.name)
|
||||
|
||||
resp = self.client.get(reverse('edit_unit', kwargs={'location': location.url()}))
|
||||
self.assertEqual(resp.status_code, 400)
|
||||
@@ -224,7 +224,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
draft_store.clone_item(html_module.location, html_module.location)
|
||||
html_module = draft_store.get_item(['i4x', 'edX', 'simple', 'html', 'test_html', None])
|
||||
|
||||
new_graceperiod = timedelta(**{'hours': 1})
|
||||
new_graceperiod = timedelta(hours=1)
|
||||
|
||||
self.assertNotIn('graceperiod', own_metadata(html_module))
|
||||
html_module.lms.graceperiod = new_graceperiod
|
||||
@@ -369,7 +369,6 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
'''
|
||||
module_store = modulestore('direct')
|
||||
import_from_xml(module_store, 'common/test/data/', ['full'])
|
||||
|
||||
effort = module_store.get_item(Location(['i4x', 'edX', 'full', 'about', 'effort', None]))
|
||||
self.assertEqual(effort.data, '6 hours')
|
||||
|
||||
@@ -617,12 +616,12 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
items = module_store.get_items(Location(['i4x', 'edX', 'full', 'vertical', None]))
|
||||
self.assertEqual(len(items), 0)
|
||||
|
||||
def verify_content_existence(self, modulestore, root_dir, location, dirname, category_name, filename_suffix=''):
|
||||
def verify_content_existence(self, store, root_dir, location, dirname, category_name, filename_suffix=''):
|
||||
filesystem = OSFS(root_dir / 'test_export')
|
||||
self.assertTrue(filesystem.exists(dirname))
|
||||
|
||||
query_loc = Location('i4x', location.org, location.course, category_name, None)
|
||||
items = modulestore.get_items(query_loc)
|
||||
items = store.get_items(query_loc)
|
||||
|
||||
for item in items:
|
||||
filesystem = OSFS(root_dir / ('test_export/' + dirname))
|
||||
@@ -768,7 +767,6 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
def test_prefetch_children(self):
|
||||
module_store = modulestore('direct')
|
||||
import_from_xml(module_store, 'common/test/data/', ['full'])
|
||||
|
||||
location = CourseDescriptor.id_to_location('edX/full/6.002_Spring_2012')
|
||||
|
||||
wrapper = MongoCollectionFindWrapper(module_store.collection.find)
|
||||
@@ -864,7 +862,7 @@ class ContentStoreTest(ModuleStoreTestCase):
|
||||
|
||||
def test_create_course_duplicate_course(self):
|
||||
"""Test new course creation - error path"""
|
||||
resp = self.client.post(reverse('create_new_course'), self.course_data)
|
||||
self.client.post(reverse('create_new_course'), self.course_data)
|
||||
resp = self.client.post(reverse('create_new_course'), self.course_data)
|
||||
data = parse_json(resp)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
@@ -872,7 +870,7 @@ class ContentStoreTest(ModuleStoreTestCase):
|
||||
|
||||
def test_create_course_duplicate_number(self):
|
||||
"""Test new course creation - error path"""
|
||||
resp = self.client.post(reverse('create_new_course'), self.course_data)
|
||||
self.client.post(reverse('create_new_course'), self.course_data)
|
||||
self.course_data['display_name'] = 'Robot Super Course Two'
|
||||
|
||||
resp = self.client.post(reverse('create_new_course'), self.course_data)
|
||||
@@ -1090,11 +1088,9 @@ class ContentStoreTest(ModuleStoreTestCase):
|
||||
json.dumps({'id': del_loc.url()}), "application/json")
|
||||
self.assertEqual(200, resp.status_code)
|
||||
|
||||
|
||||
def test_import_metadata_with_attempts_empty_string(self):
|
||||
module_store = modulestore('direct')
|
||||
import_from_xml(module_store, 'common/test/data/', ['simple'])
|
||||
|
||||
did_load_item = False
|
||||
try:
|
||||
module_store.get_item(Location(['i4x', 'edX', 'simple', 'problem', 'ps01-simple', None]))
|
||||
|
||||
@@ -224,14 +224,14 @@ def add_extra_panel_tab(tab_type, course):
|
||||
@param course: A course object from the modulestore.
|
||||
@return: Boolean indicating whether or not a tab was added and a list of tabs for the course.
|
||||
"""
|
||||
#Copy course tabs
|
||||
# Copy course tabs
|
||||
course_tabs = copy.copy(course.tabs)
|
||||
changed = False
|
||||
#Check to see if open ended panel is defined in the course
|
||||
# Check to see if open ended panel is defined in the course
|
||||
|
||||
tab_panel = EXTRA_TAB_PANELS.get(tab_type)
|
||||
if tab_panel not in course_tabs:
|
||||
#Add panel to the tabs if it is not defined
|
||||
# Add panel to the tabs if it is not defined
|
||||
course_tabs.append(tab_panel)
|
||||
changed = True
|
||||
return changed, course_tabs
|
||||
@@ -244,14 +244,14 @@ def remove_extra_panel_tab(tab_type, course):
|
||||
@param course: A course object from the modulestore.
|
||||
@return: Boolean indicating whether or not a tab was added and a list of tabs for the course.
|
||||
"""
|
||||
#Copy course tabs
|
||||
# Copy course tabs
|
||||
course_tabs = copy.copy(course.tabs)
|
||||
changed = False
|
||||
#Check to see if open ended panel is defined in the course
|
||||
# Check to see if open ended panel is defined in the course
|
||||
|
||||
tab_panel = EXTRA_TAB_PANELS.get(tab_type)
|
||||
if tab_panel in course_tabs:
|
||||
#Add panel to the tabs if it is not defined
|
||||
# Add panel to the tabs if it is not defined
|
||||
course_tabs = [ct for ct in course_tabs if ct != tab_panel]
|
||||
changed = True
|
||||
return changed, course_tabs
|
||||
|
||||
@@ -12,8 +12,8 @@ from django.core.urlresolvers import reverse
|
||||
from mitxmako.shortcuts import render_to_response
|
||||
|
||||
from xmodule.modulestore.django import modulestore
|
||||
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError, InvalidLocationError
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError, \
|
||||
InvalidLocationError
|
||||
from xmodule.modulestore import Location
|
||||
|
||||
from contentstore.course_info_model import get_course_updates, update_course_updates, delete_course_update
|
||||
@@ -33,9 +33,6 @@ from .component import OPEN_ENDED_COMPONENT_TYPES, \
|
||||
from django_comment_common.utils import seed_permissions_roles
|
||||
import datetime
|
||||
from django.utils.timezone import UTC
|
||||
|
||||
# TODO: should explicitly enumerate exports with __all__
|
||||
|
||||
__all__ = ['course_index', 'create_new_course', 'course_info',
|
||||
'course_info_updates', 'get_course_settings',
|
||||
'course_config_graders_page',
|
||||
|
||||
@@ -103,7 +103,7 @@ def clone_item(request):
|
||||
@expect_json
|
||||
def delete_item(request):
|
||||
item_location = request.POST['id']
|
||||
item_loc = Location(item_location)
|
||||
item_location = Location(item_location)
|
||||
|
||||
# check permissions for this user within this course
|
||||
if not has_access(request.user, item_location):
|
||||
@@ -124,11 +124,11 @@ def delete_item(request):
|
||||
|
||||
# cdodge: we need to remove our parent's pointer to us so that it is no longer dangling
|
||||
if delete_all_versions:
|
||||
parent_locs = modulestore('direct').get_parent_locations(item_loc, None)
|
||||
parent_locs = modulestore('direct').get_parent_locations(item_location, None)
|
||||
|
||||
for parent_loc in parent_locs:
|
||||
parent = modulestore('direct').get_item(parent_loc)
|
||||
item_url = item_loc.url()
|
||||
item_url = item_location.url()
|
||||
if item_url in parent.children:
|
||||
children = parent.children
|
||||
children.remove(item_url)
|
||||
|
||||
@@ -41,25 +41,25 @@ class CourseDetails(object):
|
||||
course.enrollment_start = descriptor.enrollment_start
|
||||
course.enrollment_end = descriptor.enrollment_end
|
||||
|
||||
temploc = course_location._replace(category='about', name='syllabus')
|
||||
temploc = course_location.replace(category='about', name='syllabus')
|
||||
try:
|
||||
course.syllabus = get_modulestore(temploc).get_item(temploc).data
|
||||
except ItemNotFoundError:
|
||||
pass
|
||||
|
||||
temploc = temploc._replace(name='overview')
|
||||
temploc = temploc.replace(name='overview')
|
||||
try:
|
||||
course.overview = get_modulestore(temploc).get_item(temploc).data
|
||||
except ItemNotFoundError:
|
||||
pass
|
||||
|
||||
temploc = temploc._replace(name='effort')
|
||||
temploc = temploc.replace(name='effort')
|
||||
try:
|
||||
course.effort = get_modulestore(temploc).get_item(temploc).data
|
||||
except ItemNotFoundError:
|
||||
pass
|
||||
|
||||
temploc = temploc._replace(name='video')
|
||||
temploc = temploc.replace(name='video')
|
||||
try:
|
||||
raw_video = get_modulestore(temploc).get_item(temploc).data
|
||||
course.intro_video = CourseDetails.parse_video_tag(raw_video)
|
||||
@@ -126,16 +126,16 @@ class CourseDetails(object):
|
||||
|
||||
# NOTE: below auto writes to the db w/o verifying that any of the fields actually changed
|
||||
# to make faster, could compare against db or could have client send over a list of which fields changed.
|
||||
temploc = Location(course_location)._replace(category='about', name='syllabus')
|
||||
temploc = Location(course_location).replace(category='about', name='syllabus')
|
||||
update_item(temploc, jsondict['syllabus'])
|
||||
|
||||
temploc = temploc._replace(name='overview')
|
||||
temploc = temploc.replace(name='overview')
|
||||
update_item(temploc, jsondict['overview'])
|
||||
|
||||
temploc = temploc._replace(name='effort')
|
||||
temploc = temploc.replace(name='effort')
|
||||
update_item(temploc, jsondict['effort'])
|
||||
|
||||
temploc = temploc._replace(name='video')
|
||||
temploc = temploc.replace(name='video')
|
||||
recomposed_video_tag = CourseDetails.recompose_video_tag(jsondict['intro_video'])
|
||||
update_item(temploc, recomposed_video_tag)
|
||||
|
||||
@@ -174,10 +174,10 @@ class CourseDetails(object):
|
||||
return result
|
||||
|
||||
|
||||
# TODO move to a more general util? Is there a better way to do the isinstance model check?
|
||||
# TODO move to a more general util?
|
||||
class CourseSettingsEncoder(json.JSONEncoder):
|
||||
def default(self, obj):
|
||||
if isinstance(obj, CourseDetails) or isinstance(obj, course_grading.CourseGradingModel):
|
||||
if isinstance(obj, (CourseDetails, course_grading.CourseGradingModel)):
|
||||
return obj.__dict__
|
||||
elif isinstance(obj, Location):
|
||||
return obj.dict()
|
||||
|
||||
@@ -235,8 +235,7 @@ PIPELINE_JS = {
|
||||
'source_filenames': sorted(
|
||||
rooted_glob(COMMON_ROOT / 'static/', 'coffee/src/**/*.js') +
|
||||
rooted_glob(PROJECT_ROOT / 'static/', 'coffee/src/**/*.js')
|
||||
) + ['js/hesitate.js', 'js/base.js',
|
||||
'js/models/feedback.js', 'js/views/feedback.js',
|
||||
) + ['js/hesitate.js', 'js/base.js', 'js/views/feedback.js',
|
||||
'js/models/section.js', 'js/views/section.js',
|
||||
'js/models/metadata_model.js', 'js/views/metadata_editor_view.js',
|
||||
'js/views/assets.js'],
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
"js/vendor/jquery.cookie.js",
|
||||
"js/vendor/json2.js",
|
||||
"js/vendor/underscore-min.js",
|
||||
"js/vendor/underscore.string.min.js",
|
||||
"js/vendor/backbone-min.js",
|
||||
"js/vendor/jquery.leanModal.min.js",
|
||||
"js/vendor/sinon-1.7.1.js",
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
describe "CMS.Models.SystemFeedback", ->
|
||||
beforeEach ->
|
||||
@model = new CMS.Models.SystemFeedback()
|
||||
|
||||
it "should have an empty message by default", ->
|
||||
expect(@model.get("message")).toEqual("")
|
||||
|
||||
it "should have an empty title by default", ->
|
||||
expect(@model.get("title")).toEqual("")
|
||||
|
||||
it "should not have an intent set by default", ->
|
||||
expect(@model.get("intent")).toBeNull()
|
||||
|
||||
|
||||
describe "CMS.Models.WarningMessage", ->
|
||||
beforeEach ->
|
||||
@model = new CMS.Models.WarningMessage()
|
||||
|
||||
it "should have the correct intent", ->
|
||||
expect(@model.get("intent")).toEqual("warning")
|
||||
|
||||
describe "CMS.Models.ErrorMessage", ->
|
||||
beforeEach ->
|
||||
@model = new CMS.Models.ErrorMessage()
|
||||
|
||||
it "should have the correct intent", ->
|
||||
expect(@model.get("intent")).toEqual("error")
|
||||
|
||||
describe "CMS.Models.ConfirmationMessage", ->
|
||||
beforeEach ->
|
||||
@model = new CMS.Models.ConfirmationMessage()
|
||||
|
||||
it "should have the correct intent", ->
|
||||
expect(@model.get("intent")).toEqual("confirmation")
|
||||
@@ -18,79 +18,105 @@ beforeEach ->
|
||||
else
|
||||
return trimmedText.indexOf(text) != -1;
|
||||
|
||||
describe "CMS.Views.Alert as base class", ->
|
||||
describe "CMS.Views.SystemFeedback", ->
|
||||
beforeEach ->
|
||||
@model = new CMS.Models.ConfirmationMessage({
|
||||
@options =
|
||||
title: "Portal"
|
||||
message: "Welcome to the Aperture Science Computer-Aided Enrichment Center"
|
||||
})
|
||||
# it will be interesting to see when this.render is called, so lets spy on it
|
||||
spyOn(CMS.Views.Alert.prototype, 'render').andCallThrough()
|
||||
@renderSpy = spyOn(CMS.Views.Alert.Confirmation.prototype, 'render').andCallThrough()
|
||||
@showSpy = spyOn(CMS.Views.Alert.Confirmation.prototype, 'show').andCallThrough()
|
||||
@hideSpy = spyOn(CMS.Views.Alert.Confirmation.prototype, 'hide').andCallThrough()
|
||||
|
||||
it "renders on initalize", ->
|
||||
view = new CMS.Views.Alert({model: @model})
|
||||
expect(view.render).toHaveBeenCalled()
|
||||
it "requires a type and an intent", ->
|
||||
neither = =>
|
||||
new CMS.Views.SystemFeedback(@options)
|
||||
noType = =>
|
||||
options = $.extend({}, @options)
|
||||
options.intent = "confirmation"
|
||||
new CMS.Views.SystemFeedback(options)
|
||||
noIntent = =>
|
||||
options = $.extend({}, @options)
|
||||
options.type = "alert"
|
||||
new CMS.Views.SystemFeedback(options)
|
||||
both = =>
|
||||
options = $.extend({}, @options)
|
||||
options.type = "alert"
|
||||
options.intent = "confirmation"
|
||||
new CMS.Views.SystemFeedback(options)
|
||||
|
||||
expect(neither).toThrow()
|
||||
expect(noType).toThrow()
|
||||
expect(noIntent).toThrow()
|
||||
expect(both).not.toThrow()
|
||||
|
||||
# for simplicity, we'll use CMS.Views.Alert.Confirmation from here on,
|
||||
# which extends and proxies to CMS.Views.SystemFeedback
|
||||
|
||||
it "does not show on initalize", ->
|
||||
view = new CMS.Views.Alert.Confirmation(@options)
|
||||
expect(@renderSpy).not.toHaveBeenCalled()
|
||||
expect(@showSpy).not.toHaveBeenCalled()
|
||||
|
||||
it "renders the template", ->
|
||||
view = new CMS.Views.Alert({model: @model})
|
||||
view = new CMS.Views.Alert.Confirmation(@options)
|
||||
view.show()
|
||||
|
||||
expect(view.$(".action-close")).toBeDefined()
|
||||
expect(view.$('.wrapper')).toBeShown()
|
||||
expect(view.$el).toContainText(@model.get("title"))
|
||||
expect(view.$el).toContainText(@model.get("message"))
|
||||
expect(view.$el).toContainText(@options.title)
|
||||
expect(view.$el).toContainText(@options.message)
|
||||
|
||||
it "close button sends a .hide() message", ->
|
||||
spyOn(CMS.Views.Alert.prototype, 'hide').andCallThrough()
|
||||
|
||||
view = new CMS.Views.Alert({model: @model})
|
||||
view = new CMS.Views.Alert.Confirmation(@options).show()
|
||||
view.$(".action-close").click()
|
||||
|
||||
expect(CMS.Views.Alert.prototype.hide).toHaveBeenCalled()
|
||||
expect(@hideSpy).toHaveBeenCalled()
|
||||
expect(view.$('.wrapper')).toBeHiding()
|
||||
|
||||
describe "CMS.Views.Prompt", ->
|
||||
beforeEach ->
|
||||
@model = new CMS.Models.ConfirmationMessage({
|
||||
title: "Portal"
|
||||
message: "Welcome to the Aperture Science Computer-Aided Enrichment Center"
|
||||
})
|
||||
|
||||
# for some reason, expect($("body")) blows up the test runner, so this test
|
||||
# just exercises the Prompt rather than asserting on anything. Best I can
|
||||
# do for now. :(
|
||||
it "changes class on body", ->
|
||||
# expect($("body")).not.toHaveClass("prompt-is-shown")
|
||||
view = new CMS.Views.Prompt({model: @model})
|
||||
view = new CMS.Views.Prompt.Confirmation({
|
||||
title: "Portal"
|
||||
message: "Welcome to the Aperture Science Computer-Aided Enrichment Center"
|
||||
})
|
||||
# expect($("body")).toHaveClass("prompt-is-shown")
|
||||
view.hide()
|
||||
# expect($("body")).not.toHaveClass("prompt-is-shown")
|
||||
|
||||
describe "CMS.Views.Alert click events", ->
|
||||
describe "CMS.Views.SystemFeedback click events", ->
|
||||
beforeEach ->
|
||||
@model = new CMS.Models.WarningMessage(
|
||||
@primaryClickSpy = jasmine.createSpy('primaryClick')
|
||||
@secondaryClickSpy = jasmine.createSpy('secondaryClick')
|
||||
@view = new CMS.Views.Notification.Warning(
|
||||
title: "Unsaved",
|
||||
message: "Your content is currently Unsaved.",
|
||||
actions:
|
||||
primary:
|
||||
text: "Save",
|
||||
class: "save-button",
|
||||
click: jasmine.createSpy('primaryClick')
|
||||
click: @primaryClickSpy
|
||||
secondary: [{
|
||||
text: "Revert",
|
||||
class: "cancel-button",
|
||||
click: jasmine.createSpy('secondaryClick')
|
||||
click: @secondaryClickSpy
|
||||
}]
|
||||
|
||||
)
|
||||
|
||||
@view = new CMS.Views.Alert({model: @model})
|
||||
@view.show()
|
||||
|
||||
it "should trigger the primary event on a primary click", ->
|
||||
@view.primaryClick()
|
||||
expect(@model.get('actions').primary.click).toHaveBeenCalled()
|
||||
@view.$(".action-primary").click()
|
||||
expect(@primaryClickSpy).toHaveBeenCalled()
|
||||
expect(@secondaryClickSpy).not.toHaveBeenCalled()
|
||||
|
||||
it "should trigger the secondary event on a secondary click", ->
|
||||
@view.secondaryClick()
|
||||
expect(@model.get('actions').secondary[0].click).toHaveBeenCalled()
|
||||
@view.$(".action-secondary").click()
|
||||
expect(@secondaryClickSpy).toHaveBeenCalled()
|
||||
expect(@primaryClickSpy).not.toHaveBeenCalled()
|
||||
|
||||
it "should apply class to primary action", ->
|
||||
expect(@view.$(".action-primary")).toHaveClass("save-button")
|
||||
@@ -100,20 +126,18 @@ describe "CMS.Views.Alert click events", ->
|
||||
|
||||
describe "CMS.Views.Notification minShown and maxShown", ->
|
||||
beforeEach ->
|
||||
@model = new CMS.Models.SystemFeedback(
|
||||
intent: "saving"
|
||||
title: "Saving"
|
||||
)
|
||||
spyOn(CMS.Views.Notification.prototype, 'show').andCallThrough()
|
||||
spyOn(CMS.Views.Notification.prototype, 'hide').andCallThrough()
|
||||
@showSpy = spyOn(CMS.Views.Notification.Saving.prototype, 'show')
|
||||
@showSpy.andCallThrough()
|
||||
@hideSpy = spyOn(CMS.Views.Notification.Saving.prototype, 'hide')
|
||||
@hideSpy.andCallThrough()
|
||||
@clock = sinon.useFakeTimers()
|
||||
|
||||
afterEach ->
|
||||
@clock.restore()
|
||||
|
||||
it "a minShown view should not hide too quickly", ->
|
||||
view = new CMS.Views.Notification({model: @model, minShown: 1000})
|
||||
expect(CMS.Views.Notification.prototype.show).toHaveBeenCalled()
|
||||
view = new CMS.Views.Notification.Saving({minShown: 1000})
|
||||
view.show()
|
||||
expect(view.$('.wrapper')).toBeShown()
|
||||
|
||||
# call hide() on it, but the minShown should prevent it from hiding right away
|
||||
@@ -125,8 +149,8 @@ describe "CMS.Views.Notification minShown and maxShown", ->
|
||||
expect(view.$('.wrapper')).toBeHiding()
|
||||
|
||||
it "a maxShown view should hide by itself", ->
|
||||
view = new CMS.Views.Notification({model: @model, maxShown: 1000})
|
||||
expect(CMS.Views.Notification.prototype.show).toHaveBeenCalled()
|
||||
view = new CMS.Views.Notification.Saving({maxShown: 1000})
|
||||
view.show()
|
||||
expect(view.$('.wrapper')).toBeShown()
|
||||
|
||||
# wait for the maxShown timeout to expire, and check again
|
||||
@@ -134,13 +158,13 @@ describe "CMS.Views.Notification minShown and maxShown", ->
|
||||
expect(view.$('.wrapper')).toBeHiding()
|
||||
|
||||
it "a minShown view can stay visible longer", ->
|
||||
view = new CMS.Views.Notification({model: @model, minShown: 1000})
|
||||
expect(CMS.Views.Notification.prototype.show).toHaveBeenCalled()
|
||||
view = new CMS.Views.Notification.Saving({minShown: 1000})
|
||||
view.show()
|
||||
expect(view.$('.wrapper')).toBeShown()
|
||||
|
||||
# wait for the minShown timeout to expire, and check again
|
||||
@clock.tick(1001)
|
||||
expect(CMS.Views.Notification.prototype.hide).not.toHaveBeenCalled()
|
||||
expect(@hideSpy).not.toHaveBeenCalled()
|
||||
expect(view.$('.wrapper')).toBeShown()
|
||||
|
||||
# can now hide immediately
|
||||
@@ -148,8 +172,8 @@ describe "CMS.Views.Notification minShown and maxShown", ->
|
||||
expect(view.$('.wrapper')).toBeHiding()
|
||||
|
||||
it "a maxShown view can hide early", ->
|
||||
view = new CMS.Views.Notification({model: @model, maxShown: 1000})
|
||||
expect(CMS.Views.Notification.prototype.show).toHaveBeenCalled()
|
||||
view = new CMS.Views.Notification.Saving({maxShown: 1000})
|
||||
view.show()
|
||||
expect(view.$('.wrapper')).toBeShown()
|
||||
|
||||
# wait 50 milliseconds, and hide it early
|
||||
@@ -162,7 +186,8 @@ describe "CMS.Views.Notification minShown and maxShown", ->
|
||||
expect(view.$('.wrapper')).toBeHiding()
|
||||
|
||||
it "a view can have both maxShown and minShown", ->
|
||||
view = new CMS.Views.Notification({model: @model, minShown: 1000, maxShown: 2000})
|
||||
view = new CMS.Views.Notification.Saving({minShown: 1000, maxShown: 2000})
|
||||
view.show()
|
||||
|
||||
# can't hide early
|
||||
@clock.tick(50)
|
||||
|
||||
@@ -18,11 +18,15 @@ $ ->
|
||||
$(document).ajaxError (event, jqXHR, ajaxSettings, thrownError) ->
|
||||
if ajaxSettings.notifyOnError is false
|
||||
return
|
||||
msg = new CMS.Models.ErrorMessage(
|
||||
if jqXHR.responseText
|
||||
message = _.str.truncate(jqXHR.responseText, 300)
|
||||
else
|
||||
message = gettext("This may be happening because of an error with our server or your internet connection. Try refreshing the page or making sure you are online.")
|
||||
msg = new CMS.Views.Notification.Error(
|
||||
"title": gettext("Studio's having trouble saving your work")
|
||||
"message": jqXHR.responseText || gettext("This may be happening because of an error with our server or your internet connection. Try refreshing the page or making sure you are online.")
|
||||
"message": message
|
||||
)
|
||||
new CMS.Views.Notification({model: msg})
|
||||
msg.show()
|
||||
|
||||
window.onTouchBasedDevice = ->
|
||||
navigator.userAgent.match /iPhone|iPod|iPad/i
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
CMS.Models.SystemFeedback = Backbone.Model.extend({
|
||||
defaults: {
|
||||
"intent": null, // "warning", "confirmation", "error", "announcement", "step-required", etc
|
||||
"title": "",
|
||||
"message": ""
|
||||
/* could also have an "actions" hash: here is an example demonstrating
|
||||
the expected structure
|
||||
"actions": {
|
||||
"primary": {
|
||||
"text": "Save",
|
||||
"class": "action-save",
|
||||
"click": function() {
|
||||
// do something when Save is clicked
|
||||
// `this` refers to the model
|
||||
}
|
||||
},
|
||||
"secondary": [
|
||||
{
|
||||
"text": "Cancel",
|
||||
"class": "action-cancel",
|
||||
"click": function() {}
|
||||
}, {
|
||||
"text": "Discard Changes",
|
||||
"class": "action-discard",
|
||||
"click": function() {}
|
||||
}
|
||||
]
|
||||
}
|
||||
*/
|
||||
}
|
||||
});
|
||||
|
||||
CMS.Models.WarningMessage = CMS.Models.SystemFeedback.extend({
|
||||
defaults: $.extend({}, CMS.Models.SystemFeedback.prototype.defaults, {
|
||||
"intent": "warning"
|
||||
})
|
||||
});
|
||||
|
||||
CMS.Models.ErrorMessage = CMS.Models.SystemFeedback.extend({
|
||||
defaults: $.extend({}, CMS.Models.SystemFeedback.prototype.defaults, {
|
||||
"intent": "error"
|
||||
})
|
||||
});
|
||||
|
||||
CMS.Models.ConfirmAssetDeleteMessage = CMS.Models.SystemFeedback.extend({
|
||||
defaults: $.extend({}, CMS.Models.SystemFeedback.prototype.defaults, {
|
||||
"intent": "warning"
|
||||
})
|
||||
});
|
||||
|
||||
CMS.Models.ConfirmationMessage = CMS.Models.SystemFeedback.extend({
|
||||
defaults: $.extend({}, CMS.Models.SystemFeedback.prototype.defaults, {
|
||||
"intent": "confirmation"
|
||||
})
|
||||
});
|
||||
@@ -22,22 +22,16 @@ CMS.Models.Section = Backbone.Model.extend({
|
||||
},
|
||||
showNotification: function() {
|
||||
if(!this.msg) {
|
||||
this.msg = new CMS.Models.SystemFeedback({
|
||||
intent: "saving",
|
||||
title: gettext("Saving…")
|
||||
});
|
||||
}
|
||||
if(!this.msgView) {
|
||||
this.msgView = new CMS.Views.Notification({
|
||||
model: this.msg,
|
||||
this.msg = new CMS.Views.Notification.Saving({
|
||||
title: gettext("Saving…"),
|
||||
closeIcon: false,
|
||||
minShown: 1250
|
||||
});
|
||||
}
|
||||
this.msgView.show();
|
||||
this.msg.show();
|
||||
},
|
||||
hideNotification: function() {
|
||||
if(!this.msgView) { return; }
|
||||
this.msgView.hide();
|
||||
if(!this.msg) { return; }
|
||||
this.msg.hide();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -9,7 +9,7 @@ function removeAsset(e){
|
||||
e.preventDefault();
|
||||
|
||||
var that = this;
|
||||
var msg = new CMS.Models.ConfirmAssetDeleteMessage({
|
||||
var msg = new CMS.Views.Prompt.Confirmation({
|
||||
title: gettext("Delete File Confirmation"),
|
||||
message: gettext("Are you sure you wish to delete this item. It cannot be reversed!\n\nAlso any content that links/refers to this item will no longer work (e.g. broken images and/or links)"),
|
||||
actions: {
|
||||
@@ -17,15 +17,17 @@ function removeAsset(e){
|
||||
text: gettext("OK"),
|
||||
click: function(view) {
|
||||
// call the back-end to actually remove the asset
|
||||
$.post(view.model.get('remove_asset_url'),
|
||||
{ 'location': view.model.get('asset_location') },
|
||||
var url = $('.asset-library').data('remove-asset-callback-url');
|
||||
var row = $(that).closest('tr');
|
||||
$.post(url,
|
||||
{ 'location': row.data('id') },
|
||||
function() {
|
||||
// show the post-commit confirmation
|
||||
$(".wrapper-alert-confirmation").addClass("is-shown").attr('aria-hidden','false');
|
||||
view.model.get('row_to_remove').remove();
|
||||
row.remove();
|
||||
analytics.track('Deleted Asset', {
|
||||
'course': course_location_analytics,
|
||||
'id': view.model.get('asset_location')
|
||||
'id': row.data('id')
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -38,24 +40,9 @@ function removeAsset(e){
|
||||
view.hide();
|
||||
}
|
||||
}]
|
||||
},
|
||||
remove_asset_url: $('.asset-library').data('remove-asset-callback-url'),
|
||||
asset_location: $(this).closest('tr').data('id'),
|
||||
row_to_remove: $(this).closest('tr')
|
||||
}
|
||||
});
|
||||
|
||||
// workaround for now. We can't spawn multiple instances of the Prompt View
|
||||
// so for now, a bit of hackery to just make sure we have a single instance
|
||||
// note: confirm_delete_prompt is in asset_index.html
|
||||
if (confirm_delete_prompt === null)
|
||||
confirm_delete_prompt = new CMS.Views.Prompt({model: msg});
|
||||
else
|
||||
{
|
||||
confirm_delete_prompt.model = msg;
|
||||
confirm_delete_prompt.show();
|
||||
}
|
||||
|
||||
return;
|
||||
return msg.show();
|
||||
}
|
||||
|
||||
function showUploadModal(e) {
|
||||
@@ -125,4 +112,4 @@ function displayFinishedUpload(xhr) {
|
||||
'course': course_location_analytics,
|
||||
'asset_url': resp.url
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,39 +1,64 @@
|
||||
CMS.Views.Alert = Backbone.View.extend({
|
||||
CMS.Views.SystemFeedback = Backbone.View.extend({
|
||||
options: {
|
||||
type: "alert",
|
||||
title: "",
|
||||
message: "",
|
||||
intent: null, // "warning", "confirmation", "error", "announcement", "step-required", etc
|
||||
type: null, // "alert", "notification", or "prompt": set by subclass
|
||||
shown: true, // is this view currently being shown?
|
||||
icon: true, // should we render an icon related to the message intent?
|
||||
closeIcon: true, // should we render a close button in the top right corner?
|
||||
minShown: 0, // length of time after this view has been shown before it can be hidden (milliseconds)
|
||||
maxShown: Infinity // length of time after this view has been shown before it will be automatically hidden (milliseconds)
|
||||
|
||||
/* could also have an "actions" hash: here is an example demonstrating
|
||||
the expected structure
|
||||
actions: {
|
||||
primary: {
|
||||
"text": "Save",
|
||||
"class": "action-save",
|
||||
"click": function(view) {
|
||||
// do something when Save is clicked
|
||||
}
|
||||
},
|
||||
secondary: [
|
||||
{
|
||||
"text": "Cancel",
|
||||
"class": "action-cancel",
|
||||
"click": function(view) {}
|
||||
}, {
|
||||
"text": "Discard Changes",
|
||||
"class": "action-discard",
|
||||
"click": function(view) {}
|
||||
}
|
||||
]
|
||||
}
|
||||
*/
|
||||
},
|
||||
initialize: function() {
|
||||
if(!this.options.type) {
|
||||
throw "SystemFeedback: type required (given " +
|
||||
JSON.stringify(this.options) + ")";
|
||||
}
|
||||
if(!this.options.intent) {
|
||||
throw "SystemFeedback: intent required (given " +
|
||||
JSON.stringify(this.options) + ")";
|
||||
}
|
||||
var tpl = $("#system-feedback-tpl").text();
|
||||
if(!tpl) {
|
||||
console.error("Couldn't load system-feedback template");
|
||||
}
|
||||
this.template = _.template(tpl);
|
||||
this.setElement($("#page-"+this.options.type));
|
||||
this.listenTo(this.model, 'change', this.render);
|
||||
return this.show();
|
||||
},
|
||||
render: function() {
|
||||
var attrs = $.extend({}, this.options, this.model.attributes);
|
||||
this.$el.html(this.template(attrs));
|
||||
return this;
|
||||
},
|
||||
events: {
|
||||
"click .action-close": "hide",
|
||||
"click .action-primary": "primaryClick",
|
||||
"click .action-secondary": "secondaryClick"
|
||||
},
|
||||
// public API: show() and hide()
|
||||
show: function() {
|
||||
clearTimeout(this.hideTimeout);
|
||||
this.options.shown = true;
|
||||
this.shownAt = new Date();
|
||||
this.render();
|
||||
if($.isNumeric(this.options.maxShown)) {
|
||||
this.hideTimeout = setTimeout($.proxy(this.hide, this),
|
||||
this.hideTimeout = setTimeout(_.bind(this.hide, this),
|
||||
this.options.maxShown);
|
||||
}
|
||||
return this;
|
||||
@@ -43,7 +68,7 @@ CMS.Views.Alert = Backbone.View.extend({
|
||||
this.options.minShown > new Date() - this.shownAt)
|
||||
{
|
||||
clearTimeout(this.hideTimeout);
|
||||
this.hideTimeout = setTimeout($.proxy(this.hide, this),
|
||||
this.hideTimeout = setTimeout(_.bind(this.hide, this),
|
||||
this.options.minShown - (new Date() - this.shownAt));
|
||||
} else {
|
||||
this.options.shown = false;
|
||||
@@ -52,40 +77,64 @@ CMS.Views.Alert = Backbone.View.extend({
|
||||
}
|
||||
return this;
|
||||
},
|
||||
primaryClick: function() {
|
||||
var actions = this.model.get("actions");
|
||||
// the rest of the API should be considered semi-private
|
||||
events: {
|
||||
"click .action-close": "hide",
|
||||
"click .action-primary": "primaryClick",
|
||||
"click .action-secondary": "secondaryClick"
|
||||
},
|
||||
render: function() {
|
||||
// there can be only one active view of a given type at a time: only
|
||||
// one alert, only one notification, only one prompt. Therefore, we'll
|
||||
// use a singleton approach.
|
||||
var parent = CMS.Views[_.str.capitalize(this.options.type)];
|
||||
if(parent && parent.active && parent.active !== this) {
|
||||
parent.active.stopListening();
|
||||
parent.active.undelegateEvents();
|
||||
}
|
||||
this.$el.html(this.template(this.options));
|
||||
parent.active = this;
|
||||
return this;
|
||||
},
|
||||
primaryClick: function(event) {
|
||||
var actions = this.options.actions;
|
||||
if(!actions) { return; }
|
||||
var primary = actions.primary;
|
||||
if(!primary) { return; }
|
||||
if(primary.click) {
|
||||
primary.click.call(this.model, this);
|
||||
primary.click.call(event.target, this, event);
|
||||
}
|
||||
},
|
||||
secondaryClick: function(e) {
|
||||
var actions = this.model.get("actions");
|
||||
secondaryClick: function(event) {
|
||||
var actions = this.options.actions;
|
||||
if(!actions) { return; }
|
||||
var secondaryList = actions.secondary;
|
||||
if(!secondaryList) { return; }
|
||||
// which secondary action was clicked?
|
||||
var i = 0; // default to the first secondary action (easier for testing)
|
||||
if(e && e.target) {
|
||||
i = _.indexOf(this.$(".action-secondary"), e.target);
|
||||
if(event && event.target) {
|
||||
i = _.indexOf(this.$(".action-secondary"), event.target);
|
||||
}
|
||||
var secondary = this.model.get("actions").secondary[i];
|
||||
var secondary = secondaryList[i];
|
||||
if(secondary.click) {
|
||||
secondary.click.call(this.model, this);
|
||||
secondary.click.call(event.target, this, event);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
CMS.Views.Notification = CMS.Views.Alert.extend({
|
||||
options: $.extend({}, CMS.Views.Alert.prototype.options, {
|
||||
CMS.Views.Alert = CMS.Views.SystemFeedback.extend({
|
||||
options: $.extend({}, CMS.Views.SystemFeedback.prototype.options, {
|
||||
type: "alert"
|
||||
})
|
||||
});
|
||||
CMS.Views.Notification = CMS.Views.SystemFeedback.extend({
|
||||
options: $.extend({}, CMS.Views.SystemFeedback.prototype.options, {
|
||||
type: "notification",
|
||||
closeIcon: false
|
||||
})
|
||||
});
|
||||
CMS.Views.Prompt = CMS.Views.Alert.extend({
|
||||
options: $.extend({}, CMS.Views.Alert.prototype.options, {
|
||||
CMS.Views.Prompt = CMS.Views.SystemFeedback.extend({
|
||||
options: $.extend({}, CMS.Views.SystemFeedback.prototype.options, {
|
||||
type: "prompt",
|
||||
closeIcon: false,
|
||||
icon: false
|
||||
@@ -98,6 +147,27 @@ CMS.Views.Prompt = CMS.Views.Alert.extend({
|
||||
$body.removeClass('prompt-is-shown');
|
||||
}
|
||||
// super() in Javascript has awkward syntax :(
|
||||
return CMS.Views.Alert.prototype.render.apply(this, arguments);
|
||||
return CMS.Views.SystemFeedback.prototype.render.apply(this, arguments);
|
||||
}
|
||||
});
|
||||
|
||||
// create CMS.Views.Alert.Warning, CMS.Views.Notification.Confirmation,
|
||||
// CMS.Views.Prompt.StepRequired, etc
|
||||
var capitalCamel, types, intents;
|
||||
capitalCamel = _.compose(_.str.capitalize, _.str.camelize);
|
||||
types = ["alert", "notification", "prompt"];
|
||||
intents = ["warning", "error", "confirmation", "announcement", "step-required", "help", "saving"];
|
||||
_.each(types, function(type) {
|
||||
_.each(intents, function(intent) {
|
||||
// "class" is a reserved word in Javascript, so use "klass" instead
|
||||
var klass, subklass;
|
||||
klass = CMS.Views[capitalCamel(type)];
|
||||
subklass = klass.extend({
|
||||
options: $.extend({}, klass.prototype.options, {
|
||||
type: type,
|
||||
intent: intent
|
||||
})
|
||||
});
|
||||
klass[capitalCamel(intent)] = subklass;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -67,7 +67,7 @@ CMS.Views.SectionEdit = Backbone.View.extend({
|
||||
showInvalidMessage: function(model, error, options) {
|
||||
model.set("name", model.previous("name"));
|
||||
var that = this;
|
||||
var msg = new CMS.Models.ErrorMessage({
|
||||
var prompt = new CMS.Views.Prompt.Error({
|
||||
title: gettext("Your change could not be saved"),
|
||||
message: error,
|
||||
actions: {
|
||||
@@ -80,6 +80,6 @@ CMS.Views.SectionEdit = Backbone.View.extend({
|
||||
}
|
||||
}
|
||||
});
|
||||
new CMS.Views.Prompt({model: msg});
|
||||
prompt.show();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -8,12 +8,6 @@
|
||||
|
||||
<%block name="jsextra">
|
||||
<script src="${static.url('js/vendor/mustache.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/views/assets.js')}"></script>
|
||||
|
||||
<script type='text/javascript'>
|
||||
// we just want a singleton
|
||||
confirm_delete_prompt = null;
|
||||
</script>
|
||||
</%block>
|
||||
|
||||
<%block name="content">
|
||||
@@ -99,7 +93,7 @@
|
||||
</td>
|
||||
<td class="delete-col">
|
||||
<a href="#" data-tooltip="${_('Delete this asset')}" class="remove-asset-button"><span class="delete-icon"></span></a>
|
||||
</td>
|
||||
</td>
|
||||
</tr>
|
||||
% endfor
|
||||
</tbody>
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
<script type="text/javascript" src="/jsi18n/"></script>
|
||||
<script type="text/javascript" src="${static.url('js/vendor/json2.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/vendor/underscore-min.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/vendor/underscore.string.min.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/vendor/backbone-min.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/vendor/markitup/jquery.markitup.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/vendor/markitup/sets/wiki/set.js')}"></script>
|
||||
@@ -54,7 +55,6 @@
|
||||
<script type="text/javascript" src="${static.url('js/vendor/CodeMirror/css.js')}"></script>
|
||||
<script type="text/javascript" src="//www.youtube.com/player_api"></script>
|
||||
|
||||
<script src="${static.url('js/models/feedback.js')}"></script>
|
||||
<script src="${static.url('js/views/feedback.js')}"></script>
|
||||
|
||||
<!-- view -->
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#pylint: disable=C0111
|
||||
#pylint: disable=W0621
|
||||
# pylint: disable=C0111
|
||||
# pylint: disable=W0621
|
||||
|
||||
from lettuce import world, step
|
||||
from .factories import *
|
||||
|
||||
@@ -58,10 +58,16 @@ def css_find(css, wait_time=5):
|
||||
|
||||
|
||||
@world.absorb
|
||||
def css_click(css_selector, index=0, attempts=5):
|
||||
def css_click(css_selector, index=0, attempts=5, success_condition=lambda:True):
|
||||
"""
|
||||
Perform a click on a CSS selector, retrying if it initially fails
|
||||
This function will return if the click worked (since it is try/excepting all errors)
|
||||
Perform a click on a CSS selector, retrying if it initially fails.
|
||||
|
||||
This function handles errors that may be thrown if the component cannot be clicked on.
|
||||
However, there are cases where an error may not be thrown, and yet the operation did not
|
||||
actually succeed. For those cases, a success_condition lambda can be supplied to verify that the click worked.
|
||||
|
||||
This function will return True if the click worked (taking into account both errors and the optional
|
||||
success_condition).
|
||||
"""
|
||||
assert is_css_present(css_selector)
|
||||
attempt = 0
|
||||
@@ -69,8 +75,9 @@ def css_click(css_selector, index=0, attempts=5):
|
||||
while attempt < attempts:
|
||||
try:
|
||||
world.css_find(css_selector)[index].click()
|
||||
result = True
|
||||
break
|
||||
if success_condition():
|
||||
result = True
|
||||
break
|
||||
except WebDriverException:
|
||||
# Occasionally, MathJax or other JavaScript can cover up
|
||||
# an element temporarily.
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import pytz
|
||||
import datetime
|
||||
import dateutil.parser
|
||||
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.http import HttpResponse
|
||||
from django.http import Http404
|
||||
from django.shortcuts import redirect
|
||||
from django.conf import settings
|
||||
from mitxmako.shortcuts import render_to_response
|
||||
@@ -22,6 +20,7 @@ LOGFIELDS = ['username', 'ip', 'event_source', 'event_type', 'event', 'agent', '
|
||||
|
||||
|
||||
def log_event(event):
|
||||
"""Write tracking event to log file, and optionally to TrackingLog model."""
|
||||
event_str = json.dumps(event)
|
||||
log.info(event_str[:settings.TRACK_MAX_EVENT])
|
||||
if settings.MITX_FEATURES.get('ENABLE_SQL_TRACKING_LOGS'):
|
||||
@@ -34,6 +33,11 @@ def log_event(event):
|
||||
|
||||
|
||||
def user_track(request):
|
||||
"""
|
||||
Log when GET call to "event" URL is made by a user.
|
||||
|
||||
GET call should provide "event_type", "event", and "page" arguments.
|
||||
"""
|
||||
try: # TODO: Do the same for many of the optional META parameters
|
||||
username = request.user.username
|
||||
except:
|
||||
@@ -50,7 +54,6 @@ def user_track(request):
|
||||
except:
|
||||
agent = ''
|
||||
|
||||
# TODO: Move a bunch of this into log_event
|
||||
event = {
|
||||
"username": username,
|
||||
"session": scookie,
|
||||
@@ -68,6 +71,7 @@ def user_track(request):
|
||||
|
||||
|
||||
def server_track(request, event_type, event, page=None):
|
||||
"""Log events related to server requests."""
|
||||
try:
|
||||
username = request.user.username
|
||||
except:
|
||||
@@ -95,9 +99,52 @@ def server_track(request, event_type, event, page=None):
|
||||
log_event(event)
|
||||
|
||||
|
||||
def task_track(request_info, task_info, event_type, event, page=None):
|
||||
"""
|
||||
Logs tracking information for events occuring within celery tasks.
|
||||
|
||||
The `event_type` is a string naming the particular event being logged,
|
||||
while `event` is a dict containing whatever additional contextual information
|
||||
is desired.
|
||||
|
||||
The `request_info` is a dict containing information about the original
|
||||
task request. Relevant keys are `username`, `ip`, `agent`, and `host`.
|
||||
While the dict is required, the values in it are not, so that {} can be
|
||||
passed in.
|
||||
|
||||
In addition, a `task_info` dict provides more information about the current
|
||||
task, to be stored with the `event` dict. This may also be an empty dict.
|
||||
|
||||
The `page` parameter is optional, and allows the name of the page to
|
||||
be provided.
|
||||
"""
|
||||
|
||||
# supplement event information with additional information
|
||||
# about the task in which it is running.
|
||||
full_event = dict(event, **task_info)
|
||||
|
||||
# All fields must be specified, in case the tracking information is
|
||||
# also saved to the TrackingLog model. Get values from the task-level
|
||||
# information, or just add placeholder values.
|
||||
event = {
|
||||
"username": request_info.get('username', 'unknown'),
|
||||
"ip": request_info.get('ip', 'unknown'),
|
||||
"event_source": "task",
|
||||
"event_type": event_type,
|
||||
"event": full_event,
|
||||
"agent": request_info.get('agent', 'unknown'),
|
||||
"page": page,
|
||||
"time": datetime.datetime.utcnow().isoformat(),
|
||||
"host": request_info.get('host', 'unknown')
|
||||
}
|
||||
|
||||
log_event(event)
|
||||
|
||||
|
||||
@login_required
|
||||
@ensure_csrf_cookie
|
||||
def view_tracking_log(request, args=''):
|
||||
"""View to output contents of TrackingLog model. For staff use only."""
|
||||
if not request.user.is_staff:
|
||||
return redirect('/')
|
||||
nlen = 100
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import re
|
||||
import json
|
||||
import logging
|
||||
import static_replace
|
||||
|
||||
@@ -15,25 +15,22 @@ This is used by capa_module.
|
||||
|
||||
from datetime import datetime
|
||||
import logging
|
||||
import math
|
||||
import numpy
|
||||
import os.path
|
||||
import re
|
||||
import sys
|
||||
|
||||
from lxml import etree
|
||||
from xml.sax.saxutils import unescape
|
||||
from copy import deepcopy
|
||||
|
||||
from .correctmap import CorrectMap
|
||||
import inputtypes
|
||||
import customrender
|
||||
from .util import contextualize_text, convert_files_to_filenames
|
||||
import xqueue_interface
|
||||
from capa.correctmap import CorrectMap
|
||||
import capa.inputtypes as inputtypes
|
||||
import capa.customrender as customrender
|
||||
from capa.util import contextualize_text, convert_files_to_filenames
|
||||
import capa.xqueue_interface as xqueue_interface
|
||||
|
||||
# to be replaced with auto-registering
|
||||
import responsetypes
|
||||
import safe_exec
|
||||
import capa.responsetypes as responsetypes
|
||||
from capa.safe_exec import safe_exec
|
||||
|
||||
# dict of tagname, Response Class -- this should come from auto-registering
|
||||
response_tag_dict = dict([(x.response_tag, x) for x in responsetypes.__all__])
|
||||
@@ -46,8 +43,8 @@ response_properties = ["codeparam", "responseparam", "answer", "openendedparam"]
|
||||
|
||||
# special problem tags which should be turned into innocuous HTML
|
||||
html_transforms = {'problem': {'tag': 'div'},
|
||||
"text": {'tag': 'span'},
|
||||
"math": {'tag': 'span'},
|
||||
'text': {'tag': 'span'},
|
||||
'math': {'tag': 'span'},
|
||||
}
|
||||
|
||||
# These should be removed from HTML output, including all subelements
|
||||
@@ -134,7 +131,6 @@ class LoncapaProblem(object):
|
||||
|
||||
self.extracted_tree = self._extract_html(self.tree)
|
||||
|
||||
|
||||
def do_reset(self):
|
||||
'''
|
||||
Reset internal state to unfinished, with no answers
|
||||
@@ -175,7 +171,7 @@ class LoncapaProblem(object):
|
||||
Return the maximum score for this problem.
|
||||
'''
|
||||
maxscore = 0
|
||||
for response, responder in self.responders.iteritems():
|
||||
for responder in self.responders.values():
|
||||
maxscore += responder.get_max_score()
|
||||
return maxscore
|
||||
|
||||
@@ -220,7 +216,7 @@ class LoncapaProblem(object):
|
||||
def ungraded_response(self, xqueue_msg, queuekey):
|
||||
'''
|
||||
Handle any responses from the xqueue that do not contain grades
|
||||
Will try to pass the queue message to all inputtypes that can handle ungraded responses
|
||||
Will try to pass the queue message to all inputtypes that can handle ungraded responses
|
||||
|
||||
Does not return any value
|
||||
'''
|
||||
@@ -230,7 +226,6 @@ class LoncapaProblem(object):
|
||||
if hasattr(the_input, 'ungraded_response'):
|
||||
the_input.ungraded_response(xqueue_msg, queuekey)
|
||||
|
||||
|
||||
def is_queued(self):
|
||||
'''
|
||||
Returns True if any part of the problem has been submitted to an external queue
|
||||
@@ -238,7 +233,6 @@ class LoncapaProblem(object):
|
||||
'''
|
||||
return any(self.correct_map.is_queued(answer_id) for answer_id in self.correct_map)
|
||||
|
||||
|
||||
def get_recentmost_queuetime(self):
|
||||
'''
|
||||
Returns a DateTime object that represents the timestamp of the most recent
|
||||
@@ -256,11 +250,11 @@ class LoncapaProblem(object):
|
||||
|
||||
return max(queuetimes)
|
||||
|
||||
|
||||
def grade_answers(self, answers):
|
||||
'''
|
||||
Grade student responses. Called by capa_module.check_problem.
|
||||
answers is a dict of all the entries from request.POST, but with the first part
|
||||
|
||||
`answers` is a dict of all the entries from request.POST, but with the first part
|
||||
of each key removed (the string before the first "_").
|
||||
|
||||
Thus, for example, input_ID123 -> ID123, and input_fromjs_ID123 -> fromjs_ID123
|
||||
@@ -270,24 +264,72 @@ class LoncapaProblem(object):
|
||||
|
||||
# if answers include File objects, convert them to filenames.
|
||||
self.student_answers = convert_files_to_filenames(answers)
|
||||
return self._grade_answers(answers)
|
||||
|
||||
def supports_rescoring(self):
|
||||
"""
|
||||
Checks that the current problem definition permits rescoring.
|
||||
|
||||
More precisely, it checks that there are no response types in
|
||||
the current problem that are not fully supported (yet) for rescoring.
|
||||
|
||||
This includes responsetypes for which the student's answer
|
||||
is not properly stored in state, i.e. file submissions. At present,
|
||||
we have no way to know if an existing response was actually a real
|
||||
answer or merely the filename of a file submitted as an answer.
|
||||
|
||||
It turns out that because rescoring is a background task, limiting
|
||||
it to responsetypes that don't support file submissions also means
|
||||
that the responsetypes are synchronous. This is convenient as it
|
||||
permits rescoring to be complete when the rescoring call returns.
|
||||
"""
|
||||
return all('filesubmission' not in responder.allowed_inputfields for responder in self.responders.values())
|
||||
|
||||
def rescore_existing_answers(self):
|
||||
"""
|
||||
Rescore student responses. Called by capa_module.rescore_problem.
|
||||
"""
|
||||
return self._grade_answers(None)
|
||||
|
||||
def _grade_answers(self, student_answers):
|
||||
"""
|
||||
Internal grading call used for checking new 'student_answers' and also
|
||||
rescoring existing student_answers.
|
||||
|
||||
For new student_answers being graded, `student_answers` is a dict of all the
|
||||
entries from request.POST, but with the first part of each key removed
|
||||
(the string before the first "_"). Thus, for example,
|
||||
input_ID123 -> ID123, and input_fromjs_ID123 -> fromjs_ID123.
|
||||
|
||||
For rescoring, `student_answers` is None.
|
||||
|
||||
Calls the Response for each question in this problem, to do the actual grading.
|
||||
"""
|
||||
# old CorrectMap
|
||||
oldcmap = self.correct_map
|
||||
|
||||
# start new with empty CorrectMap
|
||||
newcmap = CorrectMap()
|
||||
# log.debug('Responders: %s' % self.responders)
|
||||
|
||||
# Call each responsetype instance to do actual grading
|
||||
for responder in self.responders.values():
|
||||
# File objects are passed only if responsetype explicitly allows for file
|
||||
# submissions
|
||||
if 'filesubmission' in responder.allowed_inputfields:
|
||||
results = responder.evaluate_answers(answers, oldcmap)
|
||||
# File objects are passed only if responsetype explicitly allows
|
||||
# for file submissions. But we have no way of knowing if
|
||||
# student_answers contains a proper answer or the filename of
|
||||
# an earlier submission, so for now skip these entirely.
|
||||
# TODO: figure out where to get file submissions when rescoring.
|
||||
if 'filesubmission' in responder.allowed_inputfields and student_answers is None:
|
||||
raise Exception("Cannot rescore problems with possible file submissions")
|
||||
|
||||
# use 'student_answers' only if it is provided, and if it might contain a file
|
||||
# submission that would not exist in the persisted "student_answers".
|
||||
if 'filesubmission' in responder.allowed_inputfields and student_answers is not None:
|
||||
results = responder.evaluate_answers(student_answers, oldcmap)
|
||||
else:
|
||||
results = responder.evaluate_answers(convert_files_to_filenames(answers), oldcmap)
|
||||
results = responder.evaluate_answers(self.student_answers, oldcmap)
|
||||
newcmap.update(results)
|
||||
|
||||
self.correct_map = newcmap
|
||||
# log.debug('%s: in grade_answers, answers=%s, cmap=%s' % (self,answers,newcmap))
|
||||
return newcmap
|
||||
|
||||
def get_question_answers(self):
|
||||
@@ -331,7 +373,6 @@ class LoncapaProblem(object):
|
||||
html = contextualize_text(etree.tostring(self._extract_html(self.tree)), self.context)
|
||||
return html
|
||||
|
||||
|
||||
def handle_input_ajax(self, get):
|
||||
'''
|
||||
InputTypes can support specialized AJAX calls. Find the correct input and pass along the correct data
|
||||
@@ -348,8 +389,6 @@ class LoncapaProblem(object):
|
||||
log.warning("Could not find matching input for id: %s" % input_id)
|
||||
return {}
|
||||
|
||||
|
||||
|
||||
# ======= Private Methods Below ========
|
||||
|
||||
def _process_includes(self):
|
||||
@@ -359,16 +398,16 @@ class LoncapaProblem(object):
|
||||
'''
|
||||
includes = self.tree.findall('.//include')
|
||||
for inc in includes:
|
||||
file = inc.get('file')
|
||||
if file is not None:
|
||||
filename = inc.get('file')
|
||||
if filename is not None:
|
||||
try:
|
||||
# open using ModuleSystem OSFS filestore
|
||||
ifp = self.system.filestore.open(file)
|
||||
ifp = self.system.filestore.open(filename)
|
||||
except Exception as err:
|
||||
log.warning('Error %s in problem xml include: %s' % (
|
||||
err, etree.tostring(inc, pretty_print=True)))
|
||||
log.warning('Cannot find file %s in %s' % (
|
||||
file, self.system.filestore))
|
||||
filename, self.system.filestore))
|
||||
# if debugging, don't fail - just log error
|
||||
# TODO (vshnayder): need real error handling, display to users
|
||||
if not self.system.get('DEBUG'):
|
||||
@@ -381,7 +420,7 @@ class LoncapaProblem(object):
|
||||
except Exception as err:
|
||||
log.warning('Error %s in problem xml include: %s' % (
|
||||
err, etree.tostring(inc, pretty_print=True)))
|
||||
log.warning('Cannot parse XML in %s' % (file))
|
||||
log.warning('Cannot parse XML in %s' % (filename))
|
||||
# if debugging, don't fail - just log error
|
||||
# TODO (vshnayder): same as above
|
||||
if not self.system.get('DEBUG'):
|
||||
@@ -389,11 +428,11 @@ class LoncapaProblem(object):
|
||||
else:
|
||||
continue
|
||||
|
||||
# insert new XML into tree in place of inlcude
|
||||
# insert new XML into tree in place of include
|
||||
parent = inc.getparent()
|
||||
parent.insert(parent.index(inc), incxml)
|
||||
parent.remove(inc)
|
||||
log.debug('Included %s into %s' % (file, self.problem_id))
|
||||
log.debug('Included %s into %s' % (filename, self.problem_id))
|
||||
|
||||
def _extract_system_path(self, script):
|
||||
"""
|
||||
@@ -463,7 +502,7 @@ class LoncapaProblem(object):
|
||||
|
||||
if all_code:
|
||||
try:
|
||||
safe_exec.safe_exec(
|
||||
safe_exec(
|
||||
all_code,
|
||||
context,
|
||||
random_seed=self.seed,
|
||||
@@ -519,18 +558,18 @@ class LoncapaProblem(object):
|
||||
value = ""
|
||||
if self.student_answers and problemid in self.student_answers:
|
||||
value = self.student_answers[problemid]
|
||||
|
||||
|
||||
if input_id not in self.input_state:
|
||||
self.input_state[input_id] = {}
|
||||
|
||||
|
||||
# do the rendering
|
||||
state = {'value': value,
|
||||
'status': status,
|
||||
'id': input_id,
|
||||
'input_state': self.input_state[input_id],
|
||||
'feedback': {'message': msg,
|
||||
'hint': hint,
|
||||
'hintmode': hintmode, }}
|
||||
'status': status,
|
||||
'id': input_id,
|
||||
'input_state': self.input_state[input_id],
|
||||
'feedback': {'message': msg,
|
||||
'hint': hint,
|
||||
'hintmode': hintmode, }}
|
||||
|
||||
input_type_cls = inputtypes.registry.get_class_for_tag(problemtree.tag)
|
||||
# save the input type so that we can make ajax calls on it if we need to
|
||||
@@ -554,7 +593,7 @@ class LoncapaProblem(object):
|
||||
for item in problemtree:
|
||||
item_xhtml = self._extract_html(item)
|
||||
if item_xhtml is not None:
|
||||
tree.append(item_xhtml)
|
||||
tree.append(item_xhtml)
|
||||
|
||||
if tree.tag in html_transforms:
|
||||
tree.tag = html_transforms[problemtree.tag]['tag']
|
||||
|
||||
@@ -12,8 +12,8 @@ from path import path
|
||||
from cStringIO import StringIO
|
||||
from collections import defaultdict
|
||||
|
||||
from .calc import UndefinedVariable
|
||||
from .capa_problem import LoncapaProblem
|
||||
from calc import UndefinedVariable
|
||||
from capa.capa_problem import LoncapaProblem
|
||||
from mako.lookup import TemplateLookup
|
||||
|
||||
logging.basicConfig(format="%(levelname)s %(message)s")
|
||||
|
||||
@@ -1,31 +1,44 @@
|
||||
"""
|
||||
Tests to verify that CorrectMap behaves correctly
|
||||
"""
|
||||
|
||||
import unittest
|
||||
from capa.correctmap import CorrectMap
|
||||
import datetime
|
||||
|
||||
|
||||
class CorrectMapTest(unittest.TestCase):
|
||||
"""
|
||||
Tests to verify that CorrectMap behaves correctly
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
self.cmap = CorrectMap()
|
||||
|
||||
def test_set_input_properties(self):
|
||||
|
||||
# Set the correctmap properties for two inputs
|
||||
self.cmap.set(answer_id='1_2_1',
|
||||
correctness='correct',
|
||||
npoints=5,
|
||||
msg='Test message',
|
||||
hint='Test hint',
|
||||
hintmode='always',
|
||||
queuestate={'key':'secretstring',
|
||||
'time':'20130228100026'})
|
||||
self.cmap.set(
|
||||
answer_id='1_2_1',
|
||||
correctness='correct',
|
||||
npoints=5,
|
||||
msg='Test message',
|
||||
hint='Test hint',
|
||||
hintmode='always',
|
||||
queuestate={
|
||||
'key': 'secretstring',
|
||||
'time': '20130228100026'
|
||||
}
|
||||
)
|
||||
|
||||
self.cmap.set(answer_id='2_2_1',
|
||||
correctness='incorrect',
|
||||
npoints=None,
|
||||
msg=None,
|
||||
hint=None,
|
||||
hintmode=None,
|
||||
queuestate=None)
|
||||
self.cmap.set(
|
||||
answer_id='2_2_1',
|
||||
correctness='incorrect',
|
||||
npoints=None,
|
||||
msg=None,
|
||||
hint=None,
|
||||
hintmode=None,
|
||||
queuestate=None
|
||||
)
|
||||
|
||||
# Assert that each input has the expected properties
|
||||
self.assertTrue(self.cmap.is_correct('1_2_1'))
|
||||
@@ -62,7 +75,6 @@ class CorrectMapTest(unittest.TestCase):
|
||||
self.assertFalse(self.cmap.is_right_queuekey('2_2_1', ''))
|
||||
self.assertFalse(self.cmap.is_right_queuekey('2_2_1', None))
|
||||
|
||||
|
||||
def test_get_npoints(self):
|
||||
# Set the correctmap properties for 4 inputs
|
||||
# 1) correct, 5 points
|
||||
@@ -70,25 +82,35 @@ class CorrectMapTest(unittest.TestCase):
|
||||
# 3) incorrect, 5 points
|
||||
# 4) incorrect, None points
|
||||
# 5) correct, 0 points
|
||||
self.cmap.set(answer_id='1_2_1',
|
||||
correctness='correct',
|
||||
npoints=5)
|
||||
self.cmap.set(
|
||||
answer_id='1_2_1',
|
||||
correctness='correct',
|
||||
npoints=5
|
||||
)
|
||||
|
||||
self.cmap.set(answer_id='2_2_1',
|
||||
correctness='correct',
|
||||
npoints=None)
|
||||
self.cmap.set(
|
||||
answer_id='2_2_1',
|
||||
correctness='correct',
|
||||
npoints=None
|
||||
)
|
||||
|
||||
self.cmap.set(answer_id='3_2_1',
|
||||
correctness='incorrect',
|
||||
npoints=5)
|
||||
self.cmap.set(
|
||||
answer_id='3_2_1',
|
||||
correctness='incorrect',
|
||||
npoints=5
|
||||
)
|
||||
|
||||
self.cmap.set(answer_id='4_2_1',
|
||||
correctness='incorrect',
|
||||
npoints=None)
|
||||
self.cmap.set(
|
||||
answer_id='4_2_1',
|
||||
correctness='incorrect',
|
||||
npoints=None
|
||||
)
|
||||
|
||||
self.cmap.set(answer_id='5_2_1',
|
||||
correctness='correct',
|
||||
npoints=0)
|
||||
self.cmap.set(
|
||||
answer_id='5_2_1',
|
||||
correctness='correct',
|
||||
npoints=0
|
||||
)
|
||||
|
||||
# Assert that we get the expected points
|
||||
# If points assigned --> npoints
|
||||
@@ -100,7 +122,6 @@ class CorrectMapTest(unittest.TestCase):
|
||||
self.assertEqual(self.cmap.get_npoints('4_2_1'), 0)
|
||||
self.assertEqual(self.cmap.get_npoints('5_2_1'), 0)
|
||||
|
||||
|
||||
def test_set_overall_message(self):
|
||||
|
||||
# Default is an empty string string
|
||||
@@ -118,14 +139,18 @@ class CorrectMapTest(unittest.TestCase):
|
||||
|
||||
def test_update_from_correctmap(self):
|
||||
# Initialize a CorrectMap with some properties
|
||||
self.cmap.set(answer_id='1_2_1',
|
||||
correctness='correct',
|
||||
npoints=5,
|
||||
msg='Test message',
|
||||
hint='Test hint',
|
||||
hintmode='always',
|
||||
queuestate={'key':'secretstring',
|
||||
'time':'20130228100026'})
|
||||
self.cmap.set(
|
||||
answer_id='1_2_1',
|
||||
correctness='correct',
|
||||
npoints=5,
|
||||
msg='Test message',
|
||||
hint='Test hint',
|
||||
hintmode='always',
|
||||
queuestate={
|
||||
'key': 'secretstring',
|
||||
'time': '20130228100026'
|
||||
}
|
||||
)
|
||||
|
||||
self.cmap.set_overall_message("Test message")
|
||||
|
||||
@@ -133,14 +158,17 @@ class CorrectMapTest(unittest.TestCase):
|
||||
# as the first cmap
|
||||
other_cmap = CorrectMap()
|
||||
other_cmap.update(self.cmap)
|
||||
|
||||
|
||||
# Assert that it has all the same properties
|
||||
self.assertEqual(other_cmap.get_overall_message(),
|
||||
self.cmap.get_overall_message())
|
||||
|
||||
self.assertEqual(other_cmap.get_dict(),
|
||||
self.cmap.get_dict())
|
||||
self.assertEqual(
|
||||
other_cmap.get_overall_message(),
|
||||
self.cmap.get_overall_message()
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
other_cmap.get_dict(),
|
||||
self.cmap.get_dict()
|
||||
)
|
||||
|
||||
def test_update_from_invalid(self):
|
||||
# Should get an exception if we try to update() a CorrectMap
|
||||
|
||||
@@ -4,7 +4,6 @@ Tests of responsetypes
|
||||
|
||||
from datetime import datetime
|
||||
import json
|
||||
from nose.plugins.skip import SkipTest
|
||||
import os
|
||||
import random
|
||||
import unittest
|
||||
@@ -56,9 +55,18 @@ class ResponseTest(unittest.TestCase):
|
||||
self.assertEqual(result, 'incorrect',
|
||||
msg="%s should be marked incorrect" % str(input_str))
|
||||
|
||||
def _get_random_number_code(self):
|
||||
"""Returns code to be used to generate a random result."""
|
||||
return "str(random.randint(0, 1e9))"
|
||||
|
||||
def _get_random_number_result(self, seed_value):
|
||||
"""Returns a result that should be generated using the random_number_code."""
|
||||
rand = random.Random(seed_value)
|
||||
return str(rand.randint(0, 1e9))
|
||||
|
||||
|
||||
class MultiChoiceResponseTest(ResponseTest):
|
||||
from response_xml_factory import MultipleChoiceResponseXMLFactory
|
||||
from capa.tests.response_xml_factory import MultipleChoiceResponseXMLFactory
|
||||
xml_factory_class = MultipleChoiceResponseXMLFactory
|
||||
|
||||
def test_multiple_choice_grade(self):
|
||||
@@ -80,7 +88,7 @@ class MultiChoiceResponseTest(ResponseTest):
|
||||
|
||||
|
||||
class TrueFalseResponseTest(ResponseTest):
|
||||
from response_xml_factory import TrueFalseResponseXMLFactory
|
||||
from capa.tests.response_xml_factory import TrueFalseResponseXMLFactory
|
||||
xml_factory_class = TrueFalseResponseXMLFactory
|
||||
|
||||
def test_true_false_grade(self):
|
||||
@@ -120,7 +128,7 @@ class TrueFalseResponseTest(ResponseTest):
|
||||
|
||||
|
||||
class ImageResponseTest(ResponseTest):
|
||||
from response_xml_factory import ImageResponseXMLFactory
|
||||
from capa.tests.response_xml_factory import ImageResponseXMLFactory
|
||||
xml_factory_class = ImageResponseXMLFactory
|
||||
|
||||
def test_rectangle_grade(self):
|
||||
@@ -184,7 +192,7 @@ class ImageResponseTest(ResponseTest):
|
||||
|
||||
|
||||
class SymbolicResponseTest(ResponseTest):
|
||||
from response_xml_factory import SymbolicResponseXMLFactory
|
||||
from capa.tests.response_xml_factory import SymbolicResponseXMLFactory
|
||||
xml_factory_class = SymbolicResponseXMLFactory
|
||||
|
||||
def test_grade_single_input(self):
|
||||
@@ -224,8 +232,8 @@ class SymbolicResponseTest(ResponseTest):
|
||||
|
||||
def test_complex_number_grade(self):
|
||||
problem = self.build_problem(math_display=True,
|
||||
expect="[[cos(theta),i*sin(theta)],[i*sin(theta),cos(theta)]]",
|
||||
options=["matrix", "imaginary"])
|
||||
expect="[[cos(theta),i*sin(theta)],[i*sin(theta),cos(theta)]]",
|
||||
options=["matrix", "imaginary"])
|
||||
|
||||
# For LaTeX-style inputs, symmath_check() will try to contact
|
||||
# a server to convert the input to MathML.
|
||||
@@ -312,16 +320,16 @@ class SymbolicResponseTest(ResponseTest):
|
||||
# Should not allow multiple inputs, since we specify
|
||||
# only one "expect" value
|
||||
with self.assertRaises(Exception):
|
||||
problem = self.build_problem(math_display=True,
|
||||
expect="2*x+3*y",
|
||||
num_inputs=3)
|
||||
self.build_problem(math_display=True,
|
||||
expect="2*x+3*y",
|
||||
num_inputs=3)
|
||||
|
||||
def _assert_symbolic_grade(self, problem,
|
||||
student_input,
|
||||
dynamath_input,
|
||||
expected_correctness):
|
||||
student_input,
|
||||
dynamath_input,
|
||||
expected_correctness):
|
||||
input_dict = {'1_2_1': str(student_input),
|
||||
'1_2_1_dynamath': str(dynamath_input)}
|
||||
'1_2_1_dynamath': str(dynamath_input)}
|
||||
|
||||
correct_map = problem.grade_answers(input_dict)
|
||||
|
||||
@@ -330,7 +338,7 @@ class SymbolicResponseTest(ResponseTest):
|
||||
|
||||
|
||||
class OptionResponseTest(ResponseTest):
|
||||
from response_xml_factory import OptionResponseXMLFactory
|
||||
from capa.tests.response_xml_factory import OptionResponseXMLFactory
|
||||
xml_factory_class = OptionResponseXMLFactory
|
||||
|
||||
def test_grade(self):
|
||||
@@ -350,7 +358,7 @@ class FormulaResponseTest(ResponseTest):
|
||||
"""
|
||||
Test the FormulaResponse class
|
||||
"""
|
||||
from response_xml_factory import FormulaResponseXMLFactory
|
||||
from capa.tests.response_xml_factory import FormulaResponseXMLFactory
|
||||
xml_factory_class = FormulaResponseXMLFactory
|
||||
|
||||
def test_grade(self):
|
||||
@@ -570,7 +578,7 @@ class FormulaResponseTest(ResponseTest):
|
||||
|
||||
|
||||
class StringResponseTest(ResponseTest):
|
||||
from response_xml_factory import StringResponseXMLFactory
|
||||
from capa.tests.response_xml_factory import StringResponseXMLFactory
|
||||
xml_factory_class = StringResponseXMLFactory
|
||||
|
||||
def test_case_sensitive(self):
|
||||
@@ -647,19 +655,18 @@ class StringResponseTest(ResponseTest):
|
||||
hintfn="gimme_a_random_hint",
|
||||
script=textwrap.dedent("""
|
||||
def gimme_a_random_hint(answer_ids, student_answers, new_cmap, old_cmap):
|
||||
answer = str(random.randint(0, 1e9))
|
||||
answer = {code}
|
||||
new_cmap.set_hint_and_mode(answer_ids[0], answer, "always")
|
||||
|
||||
""")
|
||||
""".format(code=self._get_random_number_code()))
|
||||
)
|
||||
correct_map = problem.grade_answers({'1_2_1': '2'})
|
||||
hint = correct_map.get_hint('1_2_1')
|
||||
r = random.Random(problem.seed)
|
||||
self.assertEqual(hint, str(r.randint(0, 1e9)))
|
||||
self.assertEqual(hint, self._get_random_number_result(problem.seed))
|
||||
|
||||
|
||||
class CodeResponseTest(ResponseTest):
|
||||
from response_xml_factory import CodeResponseXMLFactory
|
||||
from capa.tests.response_xml_factory import CodeResponseXMLFactory
|
||||
xml_factory_class = CodeResponseXMLFactory
|
||||
|
||||
def setUp(self):
|
||||
@@ -673,6 +680,7 @@ class CodeResponseTest(ResponseTest):
|
||||
|
||||
@staticmethod
|
||||
def make_queuestate(key, time):
|
||||
"""Create queuestate dict"""
|
||||
timestr = datetime.strftime(time, dateformat)
|
||||
return {'key': key, 'time': timestr}
|
||||
|
||||
@@ -710,7 +718,7 @@ class CodeResponseTest(ResponseTest):
|
||||
old_cmap = CorrectMap()
|
||||
for i, answer_id in enumerate(answer_ids):
|
||||
queuekey = 1000 + i
|
||||
queuestate = CodeResponseTest.make_queuestate(1000 + i, datetime.now())
|
||||
queuestate = CodeResponseTest.make_queuestate(queuekey, datetime.now())
|
||||
old_cmap.update(CorrectMap(answer_id=answer_ids[i], queuestate=queuestate))
|
||||
|
||||
# Message format common to external graders
|
||||
@@ -771,7 +779,7 @@ class CodeResponseTest(ResponseTest):
|
||||
for i, answer_id in enumerate(answer_ids):
|
||||
queuekey = 1000 + i
|
||||
latest_timestamp = datetime.now()
|
||||
queuestate = CodeResponseTest.make_queuestate(1000 + i, latest_timestamp)
|
||||
queuestate = CodeResponseTest.make_queuestate(queuekey, latest_timestamp)
|
||||
cmap.update(CorrectMap(answer_id=answer_id, queuestate=queuestate))
|
||||
self.problem.correct_map.update(cmap)
|
||||
|
||||
@@ -796,7 +804,7 @@ class CodeResponseTest(ResponseTest):
|
||||
|
||||
|
||||
class ChoiceResponseTest(ResponseTest):
|
||||
from response_xml_factory import ChoiceResponseXMLFactory
|
||||
from capa.tests.response_xml_factory import ChoiceResponseXMLFactory
|
||||
xml_factory_class = ChoiceResponseXMLFactory
|
||||
|
||||
def test_radio_group_grade(self):
|
||||
@@ -828,7 +836,7 @@ class ChoiceResponseTest(ResponseTest):
|
||||
|
||||
|
||||
class JavascriptResponseTest(ResponseTest):
|
||||
from response_xml_factory import JavascriptResponseXMLFactory
|
||||
from capa.tests.response_xml_factory import JavascriptResponseXMLFactory
|
||||
xml_factory_class = JavascriptResponseXMLFactory
|
||||
|
||||
def test_grade(self):
|
||||
@@ -858,7 +866,7 @@ class JavascriptResponseTest(ResponseTest):
|
||||
system.can_execute_unsafe_code = lambda: False
|
||||
|
||||
with self.assertRaises(LoncapaProblemError):
|
||||
problem = self.build_problem(
|
||||
self.build_problem(
|
||||
system=system,
|
||||
generator_src="test_problem_generator.js",
|
||||
grader_src="test_problem_grader.js",
|
||||
@@ -869,7 +877,7 @@ class JavascriptResponseTest(ResponseTest):
|
||||
|
||||
|
||||
class NumericalResponseTest(ResponseTest):
|
||||
from response_xml_factory import NumericalResponseXMLFactory
|
||||
from capa.tests.response_xml_factory import NumericalResponseXMLFactory
|
||||
xml_factory_class = NumericalResponseXMLFactory
|
||||
|
||||
def test_grade_exact(self):
|
||||
@@ -961,7 +969,7 @@ class NumericalResponseTest(ResponseTest):
|
||||
|
||||
|
||||
class CustomResponseTest(ResponseTest):
|
||||
from response_xml_factory import CustomResponseXMLFactory
|
||||
from capa.tests.response_xml_factory import CustomResponseXMLFactory
|
||||
xml_factory_class = CustomResponseXMLFactory
|
||||
|
||||
def test_inline_code(self):
|
||||
@@ -1000,15 +1008,14 @@ class CustomResponseTest(ResponseTest):
|
||||
|
||||
def test_inline_randomization(self):
|
||||
# Make sure the seed from the problem gets fed into the script execution.
|
||||
inline_script = """messages[0] = str(random.randint(0, 1e9))"""
|
||||
inline_script = "messages[0] = {code}".format(code=self._get_random_number_code())
|
||||
problem = self.build_problem(answer=inline_script)
|
||||
|
||||
input_dict = {'1_2_1': '0'}
|
||||
correctmap = problem.grade_answers(input_dict)
|
||||
|
||||
input_msg = correctmap.get_msg('1_2_1')
|
||||
r = random.Random(problem.seed)
|
||||
self.assertEqual(input_msg, str(r.randint(0, 1e9)))
|
||||
self.assertEqual(input_msg, self._get_random_number_result(problem.seed))
|
||||
|
||||
def test_function_code_single_input(self):
|
||||
# For function code, we pass in these arguments:
|
||||
@@ -1241,25 +1248,23 @@ class CustomResponseTest(ResponseTest):
|
||||
def test_setup_randomization(self):
|
||||
# Ensure that the problem setup script gets the random seed from the problem.
|
||||
script = textwrap.dedent("""
|
||||
num = random.randint(0, 1e9)
|
||||
""")
|
||||
num = {code}
|
||||
""".format(code=self._get_random_number_code()))
|
||||
problem = self.build_problem(script=script)
|
||||
r = random.Random(problem.seed)
|
||||
self.assertEqual(r.randint(0, 1e9), problem.context['num'])
|
||||
self.assertEqual(problem.context['num'], self._get_random_number_result(problem.seed))
|
||||
|
||||
def test_check_function_randomization(self):
|
||||
# The check function should get random-seeded from the problem.
|
||||
script = textwrap.dedent("""
|
||||
def check_func(expect, answer_given):
|
||||
return {'ok': True, 'msg': str(random.randint(0, 1e9))}
|
||||
""")
|
||||
return {{'ok': True, 'msg': {code} }}
|
||||
""".format(code=self._get_random_number_code()))
|
||||
|
||||
problem = self.build_problem(script=script, cfn="check_func", expect="42")
|
||||
input_dict = {'1_2_1': '42'}
|
||||
correct_map = problem.grade_answers(input_dict)
|
||||
msg = correct_map.get_msg('1_2_1')
|
||||
r = random.Random(problem.seed)
|
||||
self.assertEqual(msg, str(r.randint(0, 1e9)))
|
||||
self.assertEqual(msg, self._get_random_number_result(problem.seed))
|
||||
|
||||
def test_module_imports_inline(self):
|
||||
'''
|
||||
@@ -1320,7 +1325,7 @@ class CustomResponseTest(ResponseTest):
|
||||
|
||||
|
||||
class SchematicResponseTest(ResponseTest):
|
||||
from response_xml_factory import SchematicResponseXMLFactory
|
||||
from capa.tests.response_xml_factory import SchematicResponseXMLFactory
|
||||
xml_factory_class = SchematicResponseXMLFactory
|
||||
|
||||
def test_grade(self):
|
||||
@@ -1349,11 +1354,10 @@ class SchematicResponseTest(ResponseTest):
|
||||
|
||||
def test_check_function_randomization(self):
|
||||
# The check function should get a random seed from the problem.
|
||||
script = "correct = ['correct' if (submission[0]['num'] == random.randint(0, 1e9)) else 'incorrect']"
|
||||
script = "correct = ['correct' if (submission[0]['num'] == {code}) else 'incorrect']".format(code=self._get_random_number_code())
|
||||
problem = self.build_problem(answer=script)
|
||||
|
||||
r = random.Random(problem.seed)
|
||||
submission_dict = {'num': r.randint(0, 1e9)}
|
||||
submission_dict = {'num': self._get_random_number_result(problem.seed)}
|
||||
input_dict = {'1_2_1': json.dumps(submission_dict)}
|
||||
correct_map = problem.grade_answers(input_dict)
|
||||
|
||||
@@ -1372,7 +1376,7 @@ class SchematicResponseTest(ResponseTest):
|
||||
|
||||
|
||||
class AnnotationResponseTest(ResponseTest):
|
||||
from response_xml_factory import AnnotationResponseXMLFactory
|
||||
from capa.tests.response_xml_factory import AnnotationResponseXMLFactory
|
||||
xml_factory_class = AnnotationResponseXMLFactory
|
||||
|
||||
def test_grade(self):
|
||||
@@ -1393,7 +1397,7 @@ class AnnotationResponseTest(ResponseTest):
|
||||
{'correctness': incorrect, 'points': 0, 'answers': {answer_id: 'null'}},
|
||||
]
|
||||
|
||||
for (index, test) in enumerate(tests):
|
||||
for test in tests:
|
||||
expected_correctness = test['correctness']
|
||||
expected_points = test['points']
|
||||
answers = test['answers']
|
||||
|
||||
@@ -279,7 +279,7 @@ class CapaModule(CapaFields, XModule):
|
||||
"""
|
||||
Return True/False to indicate whether to show the "Check" button.
|
||||
"""
|
||||
submitted_without_reset = (self.is_completed() and self.rerandomize == "always")
|
||||
submitted_without_reset = (self.is_submitted() and self.rerandomize == "always")
|
||||
|
||||
# If the problem is closed (past due / too many attempts)
|
||||
# then we do NOT show the "check" button
|
||||
@@ -302,7 +302,7 @@ class CapaModule(CapaFields, XModule):
|
||||
# then do NOT show the reset button.
|
||||
# If the problem hasn't been submitted yet, then do NOT show
|
||||
# the reset button.
|
||||
if (self.closed() and not is_survey_question) or not self.is_completed():
|
||||
if (self.closed() and not is_survey_question) or not self.is_submitted():
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
@@ -322,7 +322,7 @@ class CapaModule(CapaFields, XModule):
|
||||
return not self.closed()
|
||||
else:
|
||||
is_survey_question = (self.max_attempts == 0)
|
||||
needs_reset = self.is_completed() and self.rerandomize == "always"
|
||||
needs_reset = self.is_submitted() and self.rerandomize == "always"
|
||||
|
||||
# If the student has unlimited attempts, and their answers
|
||||
# are not randomized, then we do not need a save button
|
||||
@@ -424,7 +424,7 @@ class CapaModule(CapaFields, XModule):
|
||||
|
||||
# If we cannot construct the problem HTML,
|
||||
# then generate an error message instead.
|
||||
except Exception, err:
|
||||
except Exception as err:
|
||||
html = self.handle_problem_html_error(err)
|
||||
|
||||
# The convention is to pass the name of the check button
|
||||
@@ -516,13 +516,18 @@ class CapaModule(CapaFields, XModule):
|
||||
|
||||
return False
|
||||
|
||||
def is_completed(self):
|
||||
# used by conditional module
|
||||
# return self.answer_available()
|
||||
def is_submitted(self):
|
||||
"""
|
||||
Used to decide to show or hide RESET or CHECK buttons.
|
||||
|
||||
Means that student submitted problem and nothing more.
|
||||
Problem can be completely wrong.
|
||||
Pressing RESET button makes this function to return False.
|
||||
"""
|
||||
return self.lcp.done
|
||||
|
||||
def is_attempted(self):
|
||||
# used by conditional module
|
||||
"""Used by conditional module"""
|
||||
return self.attempts > 0
|
||||
|
||||
def is_correct(self):
|
||||
@@ -655,7 +660,7 @@ class CapaModule(CapaFields, XModule):
|
||||
@staticmethod
|
||||
def make_dict_of_responses(get):
|
||||
'''Make dictionary of student responses (aka "answers")
|
||||
get is POST dictionary (Djano QueryDict).
|
||||
get is POST dictionary (Django QueryDict).
|
||||
|
||||
The *get* dict has keys of the form 'x_y', which are mapped
|
||||
to key 'y' in the returned dict. For example,
|
||||
@@ -739,13 +744,13 @@ class CapaModule(CapaFields, XModule):
|
||||
# Too late. Cannot submit
|
||||
if self.closed():
|
||||
event_info['failure'] = 'closed'
|
||||
self.system.track_function('save_problem_check_fail', event_info)
|
||||
self.system.track_function('problem_check_fail', event_info)
|
||||
raise NotFoundError('Problem is closed')
|
||||
|
||||
# Problem submitted. Student should reset before checking again
|
||||
if self.done and self.rerandomize == "always":
|
||||
event_info['failure'] = 'unreset'
|
||||
self.system.track_function('save_problem_check_fail', event_info)
|
||||
self.system.track_function('problem_check_fail', event_info)
|
||||
raise NotFoundError('Problem must be reset before it can be checked again')
|
||||
|
||||
# Problem queued. Students must wait a specified waittime before they are allowed to submit
|
||||
@@ -759,6 +764,8 @@ class CapaModule(CapaFields, XModule):
|
||||
|
||||
try:
|
||||
correct_map = self.lcp.grade_answers(answers)
|
||||
self.attempts = self.attempts + 1
|
||||
self.lcp.done = True
|
||||
self.set_state_from_lcp()
|
||||
|
||||
except (StudentInputError, ResponseError, LoncapaProblemError) as inst:
|
||||
@@ -778,17 +785,13 @@ class CapaModule(CapaFields, XModule):
|
||||
|
||||
return {'success': msg}
|
||||
|
||||
except Exception, err:
|
||||
except Exception as err:
|
||||
if self.system.DEBUG:
|
||||
msg = "Error checking problem: " + str(err)
|
||||
msg += '\nTraceback:\n' + traceback.format_exc()
|
||||
return {'success': msg}
|
||||
raise
|
||||
|
||||
self.attempts = self.attempts + 1
|
||||
self.lcp.done = True
|
||||
|
||||
self.set_state_from_lcp()
|
||||
self.publish_grade()
|
||||
|
||||
# success = correct if ALL questions in this problem are correct
|
||||
@@ -802,7 +805,7 @@ class CapaModule(CapaFields, XModule):
|
||||
event_info['correct_map'] = correct_map.get_dict()
|
||||
event_info['success'] = success
|
||||
event_info['attempts'] = self.attempts
|
||||
self.system.track_function('save_problem_check', event_info)
|
||||
self.system.track_function('problem_check', event_info)
|
||||
|
||||
if hasattr(self.system, 'psychometrics_handler'): # update PsychometricsData using callback
|
||||
self.system.psychometrics_handler(self.get_state_for_lcp())
|
||||
@@ -814,12 +817,92 @@ class CapaModule(CapaFields, XModule):
|
||||
'contents': html,
|
||||
}
|
||||
|
||||
def rescore_problem(self):
|
||||
"""
|
||||
Checks whether the existing answers to a problem are correct.
|
||||
|
||||
This is called when the correct answer to a problem has been changed,
|
||||
and the grade should be re-evaluated.
|
||||
|
||||
Returns a dict with one key:
|
||||
{'success' : 'correct' | 'incorrect' | AJAX alert msg string }
|
||||
|
||||
Raises NotFoundError if called on a problem that has not yet been
|
||||
answered, or NotImplementedError if it's a problem that cannot be rescored.
|
||||
|
||||
Returns the error messages for exceptions occurring while performing
|
||||
the rescoring, rather than throwing them.
|
||||
"""
|
||||
event_info = {'state': self.lcp.get_state(), 'problem_id': self.location.url()}
|
||||
|
||||
if not self.lcp.supports_rescoring():
|
||||
event_info['failure'] = 'unsupported'
|
||||
self.system.track_function('problem_rescore_fail', event_info)
|
||||
raise NotImplementedError("Problem's definition does not support rescoring")
|
||||
|
||||
if not self.done:
|
||||
event_info['failure'] = 'unanswered'
|
||||
self.system.track_function('problem_rescore_fail', event_info)
|
||||
raise NotFoundError('Problem must be answered before it can be graded again')
|
||||
|
||||
# get old score, for comparison:
|
||||
orig_score = self.lcp.get_score()
|
||||
event_info['orig_score'] = orig_score['score']
|
||||
event_info['orig_total'] = orig_score['total']
|
||||
|
||||
try:
|
||||
correct_map = self.lcp.rescore_existing_answers()
|
||||
|
||||
except (StudentInputError, ResponseError, LoncapaProblemError) as inst:
|
||||
log.warning("Input error in capa_module:problem_rescore", exc_info=True)
|
||||
event_info['failure'] = 'input_error'
|
||||
self.system.track_function('problem_rescore_fail', event_info)
|
||||
return {'success': u"Error: {0}".format(inst.message)}
|
||||
|
||||
except Exception as err:
|
||||
event_info['failure'] = 'unexpected'
|
||||
self.system.track_function('problem_rescore_fail', event_info)
|
||||
if self.system.DEBUG:
|
||||
msg = u"Error checking problem: {0}".format(err.message)
|
||||
msg += u'\nTraceback:\n' + traceback.format_exc()
|
||||
return {'success': msg}
|
||||
raise
|
||||
|
||||
# rescoring should have no effect on attempts, so don't
|
||||
# need to increment here, or mark done. Just save.
|
||||
self.set_state_from_lcp()
|
||||
|
||||
self.publish_grade()
|
||||
|
||||
new_score = self.lcp.get_score()
|
||||
event_info['new_score'] = new_score['score']
|
||||
event_info['new_total'] = new_score['total']
|
||||
|
||||
# success = correct if ALL questions in this problem are correct
|
||||
success = 'correct'
|
||||
for answer_id in correct_map:
|
||||
if not correct_map.is_correct(answer_id):
|
||||
success = 'incorrect'
|
||||
|
||||
# NOTE: We are logging both full grading and queued-grading submissions. In the latter,
|
||||
# 'success' will always be incorrect
|
||||
event_info['correct_map'] = correct_map.get_dict()
|
||||
event_info['success'] = success
|
||||
event_info['attempts'] = self.attempts
|
||||
self.system.track_function('problem_rescore', event_info)
|
||||
|
||||
# psychometrics should be called on rescoring requests in the same way as check-problem
|
||||
if hasattr(self.system, 'psychometrics_handler'): # update PsychometricsData using callback
|
||||
self.system.psychometrics_handler(self.get_state_for_lcp())
|
||||
|
||||
return {'success': success}
|
||||
|
||||
def save_problem(self, get):
|
||||
'''
|
||||
"""
|
||||
Save the passed in answers.
|
||||
Returns a dict { 'success' : bool, ['error' : error-msg]},
|
||||
with the error key only present if success is False.
|
||||
'''
|
||||
Returns a dict { 'success' : bool, 'msg' : message }
|
||||
The message is informative on success, and an error message on failure.
|
||||
"""
|
||||
event_info = dict()
|
||||
event_info['state'] = self.lcp.get_state()
|
||||
event_info['problem_id'] = self.location.url()
|
||||
|
||||
@@ -58,7 +58,7 @@ class CombinedOpenEndedFields(object):
|
||||
state = String(help="Which step within the current task that the student is on.", default="initial",
|
||||
scope=Scope.user_state)
|
||||
student_attempts = Integer(help="Number of attempts taken by the student on this problem", default=0,
|
||||
scope=Scope.user_state)
|
||||
scope=Scope.user_state)
|
||||
ready_to_reset = Boolean(
|
||||
help="If the problem is ready to be reset or not.", default=False,
|
||||
scope=Scope.user_state
|
||||
@@ -66,7 +66,7 @@ class CombinedOpenEndedFields(object):
|
||||
attempts = Integer(
|
||||
display_name="Maximum Attempts",
|
||||
help="The number of times the student can try to answer this problem.", default=1,
|
||||
scope=Scope.settings, values = {"min" : 1 }
|
||||
scope=Scope.settings, values={"min" : 1 }
|
||||
)
|
||||
is_graded = Boolean(display_name="Graded", help="Whether or not the problem is graded.", default=False, scope=Scope.settings)
|
||||
accept_file_upload = Boolean(
|
||||
@@ -89,7 +89,7 @@ class CombinedOpenEndedFields(object):
|
||||
weight = Float(
|
||||
display_name="Problem Weight",
|
||||
help="Defines the number of points each problem is worth. If the value is not set, each problem is worth one point.",
|
||||
scope=Scope.settings, values = {"min" : 0 , "step": ".1"}
|
||||
scope=Scope.settings, values={"min" : 0 , "step": ".1"}
|
||||
)
|
||||
markdown = String(help="Markdown source of this module", scope=Scope.settings)
|
||||
|
||||
|
||||
@@ -35,8 +35,11 @@ class ConditionalModule(ConditionalFields, XModule):
|
||||
<conditional> tag attributes:
|
||||
sources - location id of required modules, separated by ';'
|
||||
|
||||
completed - map to `is_completed` module method
|
||||
submitted - map to `is_submitted` module method.
|
||||
(pressing RESET button makes this function to return False.)
|
||||
|
||||
attempted - map to `is_attempted` module method
|
||||
correct - map to `is_correct` module method
|
||||
poll_answer - map to `poll_answer` module attribute
|
||||
voted - map to `voted` module attribute
|
||||
|
||||
@@ -70,8 +73,18 @@ class ConditionalModule(ConditionalFields, XModule):
|
||||
# value: <name of module attribute>
|
||||
conditions_map = {
|
||||
'poll_answer': 'poll_answer', # poll_question attr
|
||||
'completed': 'is_completed', # capa_problem attr
|
||||
|
||||
# problem was submitted (it can be wrong)
|
||||
# if student will press reset button after that,
|
||||
# state will be reverted
|
||||
'submitted': 'is_submitted', # capa_problem attr
|
||||
|
||||
# if student attempted problem
|
||||
'attempted': 'is_attempted', # capa_problem attr
|
||||
|
||||
# if problem is full points
|
||||
'correct': 'is_correct',
|
||||
|
||||
'voted': 'voted' # poll_question attr
|
||||
}
|
||||
|
||||
|
||||
@@ -77,10 +77,8 @@ class Date(ModelType):
|
||||
else:
|
||||
return value.isoformat()
|
||||
|
||||
|
||||
TIMEDELTA_REGEX = re.compile(r'^((?P<days>\d+?) day(?:s?))?(\s)?((?P<hours>\d+?) hour(?:s?))?(\s)?((?P<minutes>\d+?) minute(?:s)?)?(\s)?((?P<seconds>\d+?) second(?:s)?)?$')
|
||||
|
||||
|
||||
class Timedelta(ModelType):
|
||||
def from_json(self, time_str):
|
||||
"""
|
||||
|
||||
@@ -84,7 +84,7 @@ class GraphicalSliderToolModule(GraphicalSliderToolFields, XModule):
|
||||
|
||||
xml = html.fromstring(html_string)
|
||||
|
||||
#substitute plot, if presented
|
||||
# substitute plot, if presented
|
||||
plot_div = '<div class="{element_class}_plot" id="{element_id}_plot" \
|
||||
style="{style}"></div>'
|
||||
plot_el = xml.xpath('//plot')
|
||||
@@ -95,7 +95,7 @@ class GraphicalSliderToolModule(GraphicalSliderToolFields, XModule):
|
||||
element_id=self.html_id,
|
||||
style=plot_el.get('style', ""))))
|
||||
|
||||
#substitute sliders
|
||||
# substitute sliders
|
||||
slider_div = '<div class="{element_class}_slider" \
|
||||
id="{element_id}_slider_{var}" \
|
||||
data-var="{var}" \
|
||||
|
||||
@@ -57,7 +57,7 @@ class HtmlDescriptor(HtmlFields, XmlDescriptor, EditingDescriptor):
|
||||
if path.endswith('.html.xml'):
|
||||
path = path[:-9] + '.html' # backcompat--look for html instead of xml
|
||||
if path.endswith('.html.html'):
|
||||
path = path[:-5] # some people like to include .html in filenames..
|
||||
path = path[:-5] # some people like to include .html in filenames..
|
||||
candidates = []
|
||||
while os.sep in path:
|
||||
candidates.append(path)
|
||||
@@ -100,9 +100,9 @@ class HtmlDescriptor(HtmlFields, XmlDescriptor, EditingDescriptor):
|
||||
pointer_path = "{category}/{url_path}".format(category='html',
|
||||
url_path=name_to_pathname(location.name))
|
||||
base = path(pointer_path).dirname()
|
||||
#log.debug("base = {0}, base.dirname={1}, filename={2}".format(base, base.dirname(), filename))
|
||||
# log.debug("base = {0}, base.dirname={1}, filename={2}".format(base, base.dirname(), filename))
|
||||
filepath = "{base}/{name}.html".format(base=base, name=filename)
|
||||
#log.debug("looking for html file for {0} at {1}".format(location, filepath))
|
||||
# log.debug("looking for html file for {0} at {1}".format(location, filepath))
|
||||
|
||||
# VS[compat]
|
||||
# TODO (cpennington): If the file doesn't exist at the right path,
|
||||
@@ -111,7 +111,7 @@ class HtmlDescriptor(HtmlFields, XmlDescriptor, EditingDescriptor):
|
||||
# online and has imported all current (fall 2012) courses from xml
|
||||
if not system.resources_fs.exists(filepath):
|
||||
candidates = cls.backcompat_paths(filepath)
|
||||
#log.debug("candidates = {0}".format(candidates))
|
||||
# log.debug("candidates = {0}".format(candidates))
|
||||
for candidate in candidates:
|
||||
if system.resources_fs.exists(candidate):
|
||||
filepath = candidate
|
||||
|
||||
@@ -196,7 +196,7 @@ class Location(_LocationBase):
|
||||
raise InvalidLocationError(location)
|
||||
|
||||
if len(location) == 5:
|
||||
args = tuple(location) + (None, )
|
||||
args = tuple(location) + (None,)
|
||||
else:
|
||||
args = tuple(location)
|
||||
|
||||
@@ -415,7 +415,7 @@ class ModuleStoreBase(ModuleStore):
|
||||
'''
|
||||
Set up the error-tracking logic.
|
||||
'''
|
||||
self._location_errors = {} # location -> ErrorLog
|
||||
self._location_errors = {} # location -> ErrorLog
|
||||
self.metadata_inheritance_cache = None
|
||||
self.modulestore_update_signal = None # can be set by runtime to route notifications of datastore changes
|
||||
|
||||
@@ -440,7 +440,7 @@ class ModuleStoreBase(ModuleStore):
|
||||
"""
|
||||
# check that item is present and raise the promised exceptions if needed
|
||||
# TODO (vshnayder): post-launch, make errors properties of items
|
||||
#self.get_item(location)
|
||||
# self.get_item(location)
|
||||
|
||||
errorlog = self._get_errorlog(location)
|
||||
return errorlog.errors
|
||||
|
||||
@@ -15,14 +15,14 @@ def as_draft(location):
|
||||
"""
|
||||
Returns the Location that is the draft for `location`
|
||||
"""
|
||||
return Location(location)._replace(revision=DRAFT)
|
||||
return Location(location).replace(revision=DRAFT)
|
||||
|
||||
|
||||
def as_published(location):
|
||||
"""
|
||||
Returns the Location that is the published version for `location`
|
||||
"""
|
||||
return Location(location)._replace(revision=None)
|
||||
return Location(location).replace(revision=None)
|
||||
|
||||
|
||||
def wrap_draft(item):
|
||||
@@ -32,7 +32,7 @@ def wrap_draft(item):
|
||||
non-draft location in either case
|
||||
"""
|
||||
setattr(item, 'is_draft', item.location.revision == DRAFT)
|
||||
item.location = item.location._replace(revision=None)
|
||||
item.location = item.location.replace(revision=None)
|
||||
return item
|
||||
|
||||
|
||||
@@ -234,7 +234,7 @@ class DraftModuleStore(ModuleStoreBase):
|
||||
# always return the draft - if available
|
||||
for draft in to_process_drafts:
|
||||
draft_loc = Location(draft["_id"])
|
||||
draft_as_non_draft_loc = draft_loc._replace(revision=None)
|
||||
draft_as_non_draft_loc = draft_loc.replace(revision=None)
|
||||
|
||||
# does non-draft exist in the collection
|
||||
# if so, replace it
|
||||
|
||||
@@ -307,7 +307,7 @@ class MongoModuleStore(ModuleStoreBase):
|
||||
location = Location(result['_id'])
|
||||
# We need to collate between draft and non-draft
|
||||
# i.e. draft verticals can have children which are not in non-draft versions
|
||||
location = location._replace(revision=None)
|
||||
location = location.replace(revision=None)
|
||||
location_url = location.url()
|
||||
if location_url in results_by_url:
|
||||
existing_children = results_by_url[location_url].get('definition', {}).get('children', [])
|
||||
|
||||
@@ -19,18 +19,18 @@ log = logging.getLogger("mitx.courseware")
|
||||
# attempts specified in xml definition overrides this.
|
||||
MAX_ATTEMPTS = 1
|
||||
|
||||
#The highest score allowed for the overall xmodule and for each rubric point
|
||||
# The highest score allowed for the overall xmodule and for each rubric point
|
||||
MAX_SCORE_ALLOWED = 50
|
||||
|
||||
#If true, default behavior is to score module as a practice problem. Otherwise, no grade at all is shown in progress
|
||||
#Metadata overrides this.
|
||||
# If true, default behavior is to score module as a practice problem. Otherwise, no grade at all is shown in progress
|
||||
# Metadata overrides this.
|
||||
IS_SCORED = False
|
||||
|
||||
#If true, then default behavior is to require a file upload or pasted link from a student for this problem.
|
||||
#Metadata overrides this.
|
||||
# If true, then default behavior is to require a file upload or pasted link from a student for this problem.
|
||||
# Metadata overrides this.
|
||||
ACCEPT_FILE_UPLOAD = False
|
||||
|
||||
#Contains all reasonable bool and case combinations of True
|
||||
# Contains all reasonable bool and case combinations of True
|
||||
TRUE_DICT = ["True", True, "TRUE", "true"]
|
||||
|
||||
HUMAN_TASK_TYPE = {
|
||||
@@ -38,8 +38,8 @@ HUMAN_TASK_TYPE = {
|
||||
'openended': "edX Assessment",
|
||||
}
|
||||
|
||||
#Default value that controls whether or not to skip basic spelling checks in the controller
|
||||
#Metadata overrides this
|
||||
# Default value that controls whether or not to skip basic spelling checks in the controller
|
||||
# Metadata overrides this
|
||||
SKIP_BASIC_CHECKS = False
|
||||
|
||||
|
||||
@@ -74,7 +74,7 @@ class CombinedOpenEndedV1Module():
|
||||
INTERMEDIATE_DONE = 'intermediate_done'
|
||||
DONE = 'done'
|
||||
|
||||
#Where the templates live for this problem
|
||||
# Where the templates live for this problem
|
||||
TEMPLATE_DIR = "combinedopenended"
|
||||
|
||||
def __init__(self, system, location, definition, descriptor,
|
||||
@@ -118,21 +118,21 @@ class CombinedOpenEndedV1Module():
|
||||
self.instance_state = instance_state
|
||||
self.display_name = instance_state.get('display_name', "Open Ended")
|
||||
|
||||
#We need to set the location here so the child modules can use it
|
||||
# We need to set the location here so the child modules can use it
|
||||
system.set('location', location)
|
||||
self.system = system
|
||||
|
||||
#Tells the system which xml definition to load
|
||||
# Tells the system which xml definition to load
|
||||
self.current_task_number = instance_state.get('current_task_number', 0)
|
||||
#This loads the states of the individual children
|
||||
# This loads the states of the individual children
|
||||
self.task_states = instance_state.get('task_states', [])
|
||||
#Overall state of the combined open ended module
|
||||
# Overall state of the combined open ended module
|
||||
self.state = instance_state.get('state', self.INITIAL)
|
||||
|
||||
self.student_attempts = instance_state.get('student_attempts', 0)
|
||||
self.weight = instance_state.get('weight', 1)
|
||||
|
||||
#Allow reset is true if student has failed the criteria to move to the next child task
|
||||
# Allow reset is true if student has failed the criteria to move to the next child task
|
||||
self.ready_to_reset = instance_state.get('ready_to_reset', False)
|
||||
self.attempts = self.instance_state.get('attempts', MAX_ATTEMPTS)
|
||||
self.is_scored = self.instance_state.get('is_graded', IS_SCORED) in TRUE_DICT
|
||||
@@ -153,7 +153,7 @@ class CombinedOpenEndedV1Module():
|
||||
rubric_string = stringify_children(definition['rubric'])
|
||||
self._max_score = self.rubric_renderer.check_if_rubric_is_parseable(rubric_string, location, MAX_SCORE_ALLOWED)
|
||||
|
||||
#Static data is passed to the child modules to render
|
||||
# Static data is passed to the child modules to render
|
||||
self.static_data = {
|
||||
'max_score': self._max_score,
|
||||
'max_attempts': self.attempts,
|
||||
@@ -243,11 +243,11 @@ class CombinedOpenEndedV1Module():
|
||||
|
||||
self.current_task_descriptor = children['descriptors'][current_task_type](self.system)
|
||||
|
||||
#This is the xml object created from the xml definition of the current task
|
||||
# This is the xml object created from the xml definition of the current task
|
||||
etree_xml = etree.fromstring(self.current_task_xml)
|
||||
|
||||
#This sends the etree_xml object through the descriptor module of the current task, and
|
||||
#returns the xml parsed by the descriptor
|
||||
# This sends the etree_xml object through the descriptor module of the current task, and
|
||||
# returns the xml parsed by the descriptor
|
||||
self.current_task_parsed_xml = self.current_task_descriptor.definition_from_xml(etree_xml, self.system)
|
||||
if current_task_state is None and self.current_task_number == 0:
|
||||
self.current_task = child_task_module(self.system, self.location,
|
||||
@@ -293,8 +293,9 @@ class CombinedOpenEndedV1Module():
|
||||
if self.current_task_number > 0:
|
||||
last_response_data = self.get_last_response(self.current_task_number - 1)
|
||||
current_response_data = self.get_current_attributes(self.current_task_number)
|
||||
|
||||
if (current_response_data['min_score_to_attempt'] > last_response_data['score']
|
||||
or current_response_data['max_score_to_attempt'] < last_response_data['score']):
|
||||
or current_response_data['max_score_to_attempt'] < last_response_data['score']):
|
||||
self.state = self.DONE
|
||||
self.ready_to_reset = True
|
||||
|
||||
@@ -307,7 +308,7 @@ class CombinedOpenEndedV1Module():
|
||||
Output: A dictionary that can be rendered into the combined open ended template.
|
||||
"""
|
||||
task_html = self.get_html_base()
|
||||
#set context variables and render template
|
||||
# set context variables and render template
|
||||
|
||||
context = {
|
||||
'items': [{'content': task_html}],
|
||||
@@ -499,7 +500,6 @@ class CombinedOpenEndedV1Module():
|
||||
"""
|
||||
changed = self.update_task_states()
|
||||
if changed:
|
||||
#return_html=self.get_html()
|
||||
pass
|
||||
return return_html
|
||||
|
||||
@@ -730,15 +730,15 @@ class CombinedOpenEndedV1Module():
|
||||
max_score = None
|
||||
score = None
|
||||
if self.is_scored and self.weight is not None:
|
||||
#Finds the maximum score of all student attempts and keeps it.
|
||||
# Finds the maximum score of all student attempts and keeps it.
|
||||
score_mat = []
|
||||
for i in xrange(0, len(self.task_states)):
|
||||
#For each task, extract all student scores on that task (each attempt for each task)
|
||||
# For each task, extract all student scores on that task (each attempt for each task)
|
||||
last_response = self.get_last_response(i)
|
||||
max_score = last_response.get('max_score', None)
|
||||
score = last_response.get('all_scores', None)
|
||||
if score is not None:
|
||||
#Convert none scores and weight scores properly
|
||||
# Convert none scores and weight scores properly
|
||||
for z in xrange(0, len(score)):
|
||||
if score[z] is None:
|
||||
score[z] = 0
|
||||
@@ -746,19 +746,19 @@ class CombinedOpenEndedV1Module():
|
||||
score_mat.append(score)
|
||||
|
||||
if len(score_mat) > 0:
|
||||
#Currently, assume that the final step is the correct one, and that those are the final scores.
|
||||
#This will change in the future, which is why the machinery above exists to extract all scores on all steps
|
||||
#TODO: better final score handling.
|
||||
# Currently, assume that the final step is the correct one, and that those are the final scores.
|
||||
# This will change in the future, which is why the machinery above exists to extract all scores on all steps
|
||||
# TODO: better final score handling.
|
||||
scores = score_mat[-1]
|
||||
score = max(scores)
|
||||
else:
|
||||
score = 0
|
||||
|
||||
if max_score is not None:
|
||||
#Weight the max score if it is not None
|
||||
# Weight the max score if it is not None
|
||||
max_score *= float(self.weight)
|
||||
else:
|
||||
#Without a max_score, we cannot have a score!
|
||||
# Without a max_score, we cannot have a score!
|
||||
score = None
|
||||
|
||||
score_dict = {
|
||||
@@ -833,7 +833,7 @@ class CombinedOpenEndedV1Descriptor():
|
||||
expected_children = ['task', 'rubric', 'prompt']
|
||||
for child in expected_children:
|
||||
if len(xml_object.xpath(child)) == 0:
|
||||
#This is a staff_facing_error
|
||||
# This is a staff_facing_error
|
||||
raise ValueError(
|
||||
"Combined Open Ended definition must include at least one '{0}' tag. Contact the learning sciences group for assistance. {1}".format(
|
||||
child, xml_object))
|
||||
@@ -848,6 +848,7 @@ class CombinedOpenEndedV1Descriptor():
|
||||
|
||||
return {'task_xml': parse_task('task'), 'prompt': parse('prompt'), 'rubric': parse('rubric')}
|
||||
|
||||
|
||||
def definition_to_xml(self, resource_fs):
|
||||
'''Return an xml element representing this definition.'''
|
||||
elt = etree.Element('combinedopenended')
|
||||
|
||||
@@ -57,13 +57,13 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
|
||||
self.queue_name = definition.get('queuename', self.DEFAULT_QUEUE)
|
||||
self.message_queue_name = definition.get('message-queuename', self.DEFAULT_MESSAGE_QUEUE)
|
||||
|
||||
#This is needed to attach feedback to specific responses later
|
||||
# This is needed to attach feedback to specific responses later
|
||||
self.submission_id = None
|
||||
self.grader_id = None
|
||||
|
||||
error_message = "No {0} found in problem xml for open ended problem. Contact the learning sciences group for assistance."
|
||||
if oeparam is None:
|
||||
#This is a staff_facing_error
|
||||
# This is a staff_facing_error
|
||||
raise ValueError(error_message.format('oeparam'))
|
||||
if self.child_prompt is None:
|
||||
raise ValueError(error_message.format('prompt'))
|
||||
@@ -95,14 +95,14 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
|
||||
grader_payload = oeparam.find('grader_payload')
|
||||
grader_payload = grader_payload.text if grader_payload is not None else ''
|
||||
|
||||
#Update grader payload with student id. If grader payload not json, error.
|
||||
# Update grader payload with student id. If grader payload not json, error.
|
||||
try:
|
||||
parsed_grader_payload = json.loads(grader_payload)
|
||||
# NOTE: self.system.location is valid because the capa_module
|
||||
# __init__ adds it (easiest way to get problem location into
|
||||
# response types)
|
||||
except TypeError, ValueError:
|
||||
#This is a dev_facing_error
|
||||
# This is a dev_facing_error
|
||||
log.exception(
|
||||
"Grader payload from external open ended grading server is not a json object! Object: {0}".format(
|
||||
grader_payload))
|
||||
@@ -148,7 +148,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
|
||||
survey_responses = event_info['survey_responses']
|
||||
for tag in ['feedback', 'submission_id', 'grader_id', 'score']:
|
||||
if tag not in survey_responses:
|
||||
#This is a student_facing_error
|
||||
# This is a student_facing_error
|
||||
return {'success': False,
|
||||
'msg': "Could not find needed tag {0} in the survey responses. Please try submitting again.".format(
|
||||
tag)}
|
||||
@@ -158,14 +158,14 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
|
||||
feedback = str(survey_responses['feedback'].encode('ascii', 'ignore'))
|
||||
score = int(survey_responses['score'])
|
||||
except:
|
||||
#This is a dev_facing_error
|
||||
# This is a dev_facing_error
|
||||
error_message = (
|
||||
"Could not parse submission id, grader id, "
|
||||
"or feedback from message_post ajax call. "
|
||||
"Here is the message data: {0}".format(survey_responses)
|
||||
)
|
||||
log.exception(error_message)
|
||||
#This is a student_facing_error
|
||||
# This is a student_facing_error
|
||||
return {'success': False, 'msg': "There was an error saving your feedback. Please contact course staff."}
|
||||
|
||||
xqueue = system.get('xqueue')
|
||||
@@ -201,14 +201,14 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
|
||||
body=json.dumps(contents)
|
||||
)
|
||||
|
||||
#Convert error to a success value
|
||||
# Convert error to a success value
|
||||
success = True
|
||||
if error:
|
||||
success = False
|
||||
|
||||
self.child_state = self.DONE
|
||||
|
||||
#This is a student_facing_message
|
||||
# This is a student_facing_message
|
||||
return {'success': success, 'msg': "Successfully submitted your feedback."}
|
||||
|
||||
def send_to_grader(self, submission, system):
|
||||
@@ -249,7 +249,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
|
||||
'submission_time': qtime,
|
||||
}
|
||||
|
||||
#Update contents with student response and student info
|
||||
# Update contents with student response and student info
|
||||
contents.update({
|
||||
'student_info': json.dumps(student_info),
|
||||
'student_response': submission,
|
||||
@@ -369,21 +369,21 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
|
||||
|
||||
for tag in ['success', 'feedback', 'submission_id', 'grader_id']:
|
||||
if tag not in response_items:
|
||||
#This is a student_facing_error
|
||||
# This is a student_facing_error
|
||||
return format_feedback('errors', 'Error getting feedback from grader.')
|
||||
|
||||
feedback_items = response_items['feedback']
|
||||
try:
|
||||
feedback = json.loads(feedback_items)
|
||||
except (TypeError, ValueError):
|
||||
#This is a dev_facing_error
|
||||
# This is a dev_facing_error
|
||||
log.exception("feedback_items from external open ended grader have invalid json {0}".format(feedback_items))
|
||||
#This is a student_facing_error
|
||||
# This is a student_facing_error
|
||||
return format_feedback('errors', 'Error getting feedback from grader.')
|
||||
|
||||
if response_items['success']:
|
||||
if len(feedback) == 0:
|
||||
#This is a student_facing_error
|
||||
# This is a student_facing_error
|
||||
return format_feedback('errors', 'No feedback available from grader.')
|
||||
|
||||
for tag in do_not_render:
|
||||
@@ -393,7 +393,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
|
||||
feedback_lst = sorted(feedback.items(), key=get_priority)
|
||||
feedback_list_part1 = u"\n".join(format_feedback(k, v) for k, v in feedback_lst)
|
||||
else:
|
||||
#This is a student_facing_error
|
||||
# This is a student_facing_error
|
||||
feedback_list_part1 = format_feedback('errors', response_items['feedback'])
|
||||
|
||||
feedback_list_part2 = (u"\n".join([format_feedback_hidden(feedback_type, value)
|
||||
@@ -470,7 +470,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
|
||||
try:
|
||||
score_result = json.loads(score_msg)
|
||||
except (TypeError, ValueError):
|
||||
#This is a dev_facing_error
|
||||
# This is a dev_facing_error
|
||||
error_message = ("External open ended grader message should be a JSON-serialized dict."
|
||||
" Received score_msg = {0}".format(score_msg))
|
||||
log.error(error_message)
|
||||
@@ -478,7 +478,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
|
||||
return fail
|
||||
|
||||
if not isinstance(score_result, dict):
|
||||
#This is a dev_facing_error
|
||||
# This is a dev_facing_error
|
||||
error_message = ("External open ended grader message should be a JSON-serialized dict."
|
||||
" Received score_result = {0}".format(score_result))
|
||||
log.error(error_message)
|
||||
@@ -487,13 +487,13 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
|
||||
|
||||
for tag in ['score', 'feedback', 'grader_type', 'success', 'grader_id', 'submission_id']:
|
||||
if tag not in score_result:
|
||||
#This is a dev_facing_error
|
||||
# This is a dev_facing_error
|
||||
error_message = ("External open ended grader message is missing required tag: {0}"
|
||||
.format(tag))
|
||||
log.error(error_message)
|
||||
fail['feedback'] = error_message
|
||||
return fail
|
||||
#This is to support peer grading
|
||||
# This is to support peer grading
|
||||
if isinstance(score_result['score'], list):
|
||||
feedback_items = []
|
||||
rubric_scores = []
|
||||
@@ -529,7 +529,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
|
||||
feedback = feedback_items
|
||||
score = int(median(score_result['score']))
|
||||
else:
|
||||
#This is for instructor and ML grading
|
||||
# This is for instructor and ML grading
|
||||
feedback, rubric_score = self._format_feedback(score_result, system)
|
||||
score = score_result['score']
|
||||
rubric_scores = [rubric_score]
|
||||
@@ -608,9 +608,9 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
|
||||
}
|
||||
|
||||
if dispatch not in handlers:
|
||||
#This is a dev_facing_error
|
||||
# This is a dev_facing_error
|
||||
log.error("Cannot find {0} in handlers in handle_ajax function for open_ended_module.py".format(dispatch))
|
||||
#This is a dev_facing_error
|
||||
# This is a dev_facing_error
|
||||
return json.dumps({'error': 'Error handling action. Please try again.', 'success': False})
|
||||
|
||||
before = self.get_progress()
|
||||
@@ -659,10 +659,10 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
|
||||
self.send_to_grader(get['student_answer'], system)
|
||||
self.change_state(self.ASSESSING)
|
||||
else:
|
||||
#Error message already defined
|
||||
# Error message already defined
|
||||
success = False
|
||||
else:
|
||||
#This is a student_facing_error
|
||||
# This is a student_facing_error
|
||||
error_message = "There was a problem saving the image in your submission. Please try a different image, or try pasting a link to an image into the answer box."
|
||||
|
||||
return {
|
||||
@@ -679,7 +679,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
|
||||
"""
|
||||
queuekey = get['queuekey']
|
||||
score_msg = get['xqueue_body']
|
||||
#TODO: Remove need for cmap
|
||||
# TODO: Remove need for cmap
|
||||
self._update_score(score_msg, queuekey, system)
|
||||
|
||||
return dict() # No AJAX return is needed
|
||||
@@ -690,7 +690,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
|
||||
Input: Modulesystem object
|
||||
Output: Rendered HTML
|
||||
"""
|
||||
#set context variables and render template
|
||||
# set context variables and render template
|
||||
eta_string = None
|
||||
if self.child_state != self.INITIAL:
|
||||
latest = self.latest_answer()
|
||||
@@ -749,7 +749,7 @@ class OpenEndedDescriptor():
|
||||
"""
|
||||
for child in ['openendedparam']:
|
||||
if len(xml_object.xpath(child)) != 1:
|
||||
#This is a staff_facing_error
|
||||
# This is a staff_facing_error
|
||||
raise ValueError(
|
||||
"Open Ended definition must include exactly one '{0}' tag. Contact the learning sciences group for assistance.".format(
|
||||
child))
|
||||
|
||||
@@ -54,7 +54,7 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild):
|
||||
@param system: Modulesystem
|
||||
@return: Rendered HTML
|
||||
"""
|
||||
#set context variables and render template
|
||||
# set context variables and render template
|
||||
if self.child_state != self.INITIAL:
|
||||
latest = self.latest_answer()
|
||||
previous_answer = latest if latest is not None else ''
|
||||
@@ -93,9 +93,9 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild):
|
||||
}
|
||||
|
||||
if dispatch not in handlers:
|
||||
#This is a dev_facing_error
|
||||
# This is a dev_facing_error
|
||||
log.error("Cannot find {0} in handlers in handle_ajax function for open_ended_module.py".format(dispatch))
|
||||
#This is a dev_facing_error
|
||||
# This is a dev_facing_error
|
||||
return json.dumps({'error': 'Error handling action. Please try again.', 'success': False})
|
||||
|
||||
before = self.get_progress()
|
||||
@@ -129,7 +129,7 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild):
|
||||
elif self.child_state in (self.POST_ASSESSMENT, self.DONE):
|
||||
context['read_only'] = True
|
||||
else:
|
||||
#This is a dev_facing_error
|
||||
# This is a dev_facing_error
|
||||
raise ValueError("Self assessment module is in an illegal state '{0}'".format(self.child_state))
|
||||
|
||||
return system.render_template('{0}/self_assessment_rubric.html'.format(self.TEMPLATE_DIR), context)
|
||||
@@ -155,7 +155,7 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild):
|
||||
elif self.child_state == self.DONE:
|
||||
context['read_only'] = True
|
||||
else:
|
||||
#This is a dev_facing_error
|
||||
# This is a dev_facing_error
|
||||
raise ValueError("Self Assessment module is in an illegal state '{0}'".format(self.child_state))
|
||||
|
||||
return system.render_template('{0}/self_assessment_hint.html'.format(self.TEMPLATE_DIR), context)
|
||||
@@ -190,10 +190,10 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild):
|
||||
self.new_history_entry(get['student_answer'])
|
||||
self.change_state(self.ASSESSING)
|
||||
else:
|
||||
#Error message already defined
|
||||
# Error message already defined
|
||||
success = False
|
||||
else:
|
||||
#This is a student_facing_error
|
||||
# This is a student_facing_error
|
||||
error_message = "There was a problem saving the image in your submission. Please try a different image, or try pasting a link to an image into the answer box."
|
||||
|
||||
return {
|
||||
@@ -227,12 +227,12 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild):
|
||||
for i in xrange(0, len(score_list)):
|
||||
score_list[i] = int(score_list[i])
|
||||
except ValueError:
|
||||
#This is a dev_facing_error
|
||||
# This is a dev_facing_error
|
||||
log.error("Non-integer score value passed to save_assessment ,or no score list present.")
|
||||
#This is a student_facing_error
|
||||
# This is a student_facing_error
|
||||
return {'success': False, 'error': "Error saving your score. Please notify course staff."}
|
||||
|
||||
#Record score as assessment and rubric scores as post assessment
|
||||
# Record score as assessment and rubric scores as post assessment
|
||||
self.record_latest_score(score)
|
||||
self.record_latest_post_assessment(json.dumps(score_list))
|
||||
|
||||
@@ -272,7 +272,7 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild):
|
||||
try:
|
||||
rubric_scores = json.loads(latest_post_assessment)
|
||||
except:
|
||||
#This is a dev_facing_error
|
||||
# This is a dev_facing_error
|
||||
log.error("Cannot parse rubric scores in self assessment module from {0}".format(latest_post_assessment))
|
||||
rubric_scores = []
|
||||
return [rubric_scores]
|
||||
@@ -306,7 +306,7 @@ class SelfAssessmentDescriptor():
|
||||
expected_children = []
|
||||
for child in expected_children:
|
||||
if len(xml_object.xpath(child)) != 1:
|
||||
#This is a staff_facing_error
|
||||
# This is a staff_facing_error
|
||||
raise ValueError(
|
||||
"Self assessment definition must include exactly one '{0}' tag. Contact the learning sciences group for assistance.".format(
|
||||
child))
|
||||
|
||||
@@ -62,7 +62,7 @@ class SequenceModule(SequenceFields, XModule):
|
||||
progress = reduce(Progress.add_counts, progresses)
|
||||
return progress
|
||||
|
||||
def handle_ajax(self, dispatch, get): # TODO: bounds checking
|
||||
def handle_ajax(self, dispatch, get): # TODO: bounds checking
|
||||
''' get = request.POST instance '''
|
||||
if dispatch == 'goto_position':
|
||||
self.position = int(get['position'])
|
||||
|
||||
@@ -4,6 +4,7 @@ This module has utility functions for gathering up the static content
|
||||
that is defined by XModules and XModuleDescriptors (javascript and css)
|
||||
"""
|
||||
|
||||
import logging
|
||||
import hashlib
|
||||
import os
|
||||
import errno
|
||||
@@ -15,6 +16,9 @@ from path import path
|
||||
from xmodule.x_module import XModuleDescriptor
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def write_module_styles(output_root):
|
||||
return _write_styles('.xmodule_display', output_root, _list_modules())
|
||||
|
||||
@@ -121,18 +125,32 @@ def _write_js(output_root, classes):
|
||||
type=filetype)
|
||||
contents[filename] = fragment
|
||||
|
||||
_write_files(output_root, contents)
|
||||
_write_files(output_root, contents, {'.coffee': '.js'})
|
||||
|
||||
return [output_root / filename for filename in contents.keys()]
|
||||
|
||||
|
||||
def _write_files(output_root, contents):
|
||||
def _write_files(output_root, contents, generated_suffix_map=None):
|
||||
_ensure_dir(output_root)
|
||||
for extra_file in set(output_root.files()) - set(contents.keys()):
|
||||
extra_file.remove_p()
|
||||
to_delete = set(file.basename() for file in output_root.files()) - set(contents.keys())
|
||||
|
||||
if generated_suffix_map:
|
||||
for output_file in contents.keys():
|
||||
for suffix, generated_suffix in generated_suffix_map.items():
|
||||
if output_file.endswith(suffix):
|
||||
to_delete.discard(output_file.replace(suffix, generated_suffix))
|
||||
|
||||
for extra_file in to_delete:
|
||||
(output_root / extra_file).remove_p()
|
||||
|
||||
for filename, file_content in contents.iteritems():
|
||||
(output_root / filename).write_bytes(file_content)
|
||||
output_file = output_root / filename
|
||||
|
||||
if not output_file.isfile() or output_file.read_md5() != hashlib.md5(file_content).digest():
|
||||
LOG.debug("Writing %s", output_file)
|
||||
output_file.write_bytes(file_content)
|
||||
else:
|
||||
LOG.debug("%s unchanged, skipping", output_file)
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
@@ -55,7 +55,7 @@ class CustomTagDescriptor(RawDescriptor):
|
||||
params = dict(xmltree.items())
|
||||
|
||||
# cdodge: look up the template as a module
|
||||
template_loc = self.location._replace(category='custom_tag_template', name=template_name)
|
||||
template_loc = self.location.replace(category='custom_tag_template', name=template_name)
|
||||
|
||||
template_module = modulestore().get_instance(system.course_id, template_loc)
|
||||
template_module_data = template_module.data
|
||||
|
||||
@@ -29,14 +29,14 @@ open_ended_grading_interface = {
|
||||
}
|
||||
|
||||
|
||||
def test_system():
|
||||
def get_test_system():
|
||||
"""
|
||||
Construct a test ModuleSystem instance.
|
||||
|
||||
By default, the render_template() method simply returns the repr of the
|
||||
context it is passed. You can override this behavior by monkey patching::
|
||||
|
||||
system = test_system()
|
||||
system = get_test_system()
|
||||
system.render_template = my_render_func
|
||||
|
||||
where `my_render_func` is a function of the form my_render_func(template, context).
|
||||
|
||||
@@ -8,7 +8,7 @@ from mock import Mock
|
||||
from xmodule.annotatable_module import AnnotatableModule
|
||||
from xmodule.modulestore import Location
|
||||
|
||||
from . import test_system
|
||||
from . import get_test_system
|
||||
|
||||
class AnnotatableModuleTestCase(unittest.TestCase):
|
||||
location = Location(["i4x", "edX", "toy", "annotatable", "guided_discussion"])
|
||||
@@ -32,7 +32,7 @@ class AnnotatableModuleTestCase(unittest.TestCase):
|
||||
module_data = {'data': sample_xml, 'location': location}
|
||||
|
||||
def setUp(self):
|
||||
self.annotatable = AnnotatableModule(test_system(), self.descriptor, self.module_data)
|
||||
self.annotatable = AnnotatableModule(get_test_system(), self.descriptor, self.module_data)
|
||||
|
||||
def test_annotation_data_attr(self):
|
||||
el = etree.fromstring('<annotation title="bar" body="foo" problem="0">test</annotation>')
|
||||
|
||||
@@ -17,8 +17,9 @@ from xmodule.modulestore import Location
|
||||
|
||||
from django.http import QueryDict
|
||||
|
||||
from . import test_system
|
||||
from . import get_test_system
|
||||
from pytz import UTC
|
||||
from capa.correctmap import CorrectMap
|
||||
|
||||
|
||||
class CapaFactory(object):
|
||||
@@ -111,7 +112,7 @@ class CapaFactory(object):
|
||||
# since everything else is a string.
|
||||
model_data['attempts'] = int(attempts)
|
||||
|
||||
system = test_system()
|
||||
system = get_test_system()
|
||||
system.render_template = Mock(return_value="<div>Test Template HTML</div>")
|
||||
module = CapaModule(system, descriptor, model_data)
|
||||
|
||||
@@ -597,6 +598,85 @@ class CapaModuleTest(unittest.TestCase):
|
||||
# Expect that the problem was NOT reset
|
||||
self.assertTrue('success' in result and not result['success'])
|
||||
|
||||
def test_rescore_problem_correct(self):
|
||||
|
||||
module = CapaFactory.create(attempts=1, done=True)
|
||||
|
||||
# Simulate that all answers are marked correct, no matter
|
||||
# what the input is, by patching LoncapaResponse.evaluate_answers()
|
||||
with patch('capa.responsetypes.LoncapaResponse.evaluate_answers') as mock_evaluate_answers:
|
||||
mock_evaluate_answers.return_value = CorrectMap(CapaFactory.answer_key(), 'correct')
|
||||
result = module.rescore_problem()
|
||||
|
||||
# Expect that the problem is marked correct
|
||||
self.assertEqual(result['success'], 'correct')
|
||||
|
||||
# Expect that we get no HTML
|
||||
self.assertFalse('contents' in result)
|
||||
|
||||
# Expect that the number of attempts is not incremented
|
||||
self.assertEqual(module.attempts, 1)
|
||||
|
||||
def test_rescore_problem_incorrect(self):
|
||||
# make sure it also works when attempts have been reset,
|
||||
# so add this to the test:
|
||||
module = CapaFactory.create(attempts=0, done=True)
|
||||
|
||||
# Simulate that all answers are marked incorrect, no matter
|
||||
# what the input is, by patching LoncapaResponse.evaluate_answers()
|
||||
with patch('capa.responsetypes.LoncapaResponse.evaluate_answers') as mock_evaluate_answers:
|
||||
mock_evaluate_answers.return_value = CorrectMap(CapaFactory.answer_key(), 'incorrect')
|
||||
result = module.rescore_problem()
|
||||
|
||||
# Expect that the problem is marked incorrect
|
||||
self.assertEqual(result['success'], 'incorrect')
|
||||
|
||||
# Expect that the number of attempts is not incremented
|
||||
self.assertEqual(module.attempts, 0)
|
||||
|
||||
def test_rescore_problem_not_done(self):
|
||||
# Simulate that the problem is NOT done
|
||||
module = CapaFactory.create(done=False)
|
||||
|
||||
# Try to rescore the problem, and get exception
|
||||
with self.assertRaises(xmodule.exceptions.NotFoundError):
|
||||
module.rescore_problem()
|
||||
|
||||
def test_rescore_problem_not_supported(self):
|
||||
module = CapaFactory.create(done=True)
|
||||
|
||||
# Try to rescore the problem, and get exception
|
||||
with patch('capa.capa_problem.LoncapaProblem.supports_rescoring') as mock_supports_rescoring:
|
||||
mock_supports_rescoring.return_value = False
|
||||
with self.assertRaises(NotImplementedError):
|
||||
module.rescore_problem()
|
||||
|
||||
def _rescore_problem_error_helper(self, exception_class):
|
||||
"""Helper to allow testing all errors that rescoring might return."""
|
||||
# Create the module
|
||||
module = CapaFactory.create(attempts=1, done=True)
|
||||
|
||||
# Simulate answering a problem that raises the exception
|
||||
with patch('capa.capa_problem.LoncapaProblem.rescore_existing_answers') as mock_rescore:
|
||||
mock_rescore.side_effect = exception_class(u'test error \u03a9')
|
||||
result = module.rescore_problem()
|
||||
|
||||
# Expect an AJAX alert message in 'success'
|
||||
expected_msg = u'Error: test error \u03a9'
|
||||
self.assertEqual(result['success'], expected_msg)
|
||||
|
||||
# Expect that the number of attempts is NOT incremented
|
||||
self.assertEqual(module.attempts, 1)
|
||||
|
||||
def test_rescore_problem_student_input_error(self):
|
||||
self._rescore_problem_error_helper(StudentInputError)
|
||||
|
||||
def test_rescore_problem_problem_error(self):
|
||||
self._rescore_problem_error_helper(LoncapaProblemError)
|
||||
|
||||
def test_rescore_problem_response_error(self):
|
||||
self._rescore_problem_error_helper(ResponseError)
|
||||
|
||||
def test_save_problem(self):
|
||||
module = CapaFactory.create(done=False)
|
||||
|
||||
@@ -922,7 +1002,7 @@ class CapaModuleTest(unittest.TestCase):
|
||||
# is asked to render itself as HTML
|
||||
module.lcp.get_html = Mock(side_effect=Exception("Test"))
|
||||
|
||||
# Stub out the test_system rendering function
|
||||
# Stub out the get_test_system rendering function
|
||||
module.system.render_template = Mock(return_value="<div>Test Template HTML</div>")
|
||||
|
||||
# Turn off DEBUG
|
||||
|
||||
@@ -18,7 +18,7 @@ import logging
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
from . import test_system
|
||||
from . import get_test_system
|
||||
|
||||
ORG = 'edX'
|
||||
COURSE = 'open_ended' # name of directory with course data
|
||||
@@ -68,7 +68,7 @@ class OpenEndedChildTest(unittest.TestCase):
|
||||
descriptor = Mock()
|
||||
|
||||
def setUp(self):
|
||||
self.test_system = test_system()
|
||||
self.test_system = get_test_system()
|
||||
self.openendedchild = OpenEndedChild(self.test_system, self.location,
|
||||
self.definition, self.descriptor, self.static_data, self.metadata)
|
||||
|
||||
@@ -192,7 +192,7 @@ class OpenEndedModuleTest(unittest.TestCase):
|
||||
descriptor = Mock()
|
||||
|
||||
def setUp(self):
|
||||
self.test_system = test_system()
|
||||
self.test_system = get_test_system()
|
||||
|
||||
self.test_system.location = self.location
|
||||
self.mock_xqueue = MagicMock()
|
||||
@@ -367,7 +367,7 @@ class CombinedOpenEndedModuleTest(unittest.TestCase):
|
||||
definition = {'prompt': etree.XML(prompt), 'rubric': etree.XML(rubric), 'task_xml': [task_xml1, task_xml2]}
|
||||
full_definition = definition_template.format(prompt=prompt, rubric=rubric, task1=task_xml1, task2=task_xml2)
|
||||
descriptor = Mock(data=full_definition)
|
||||
test_system = test_system()
|
||||
test_system = get_test_system()
|
||||
combinedoe_container = CombinedOpenEndedModule(
|
||||
test_system,
|
||||
descriptor,
|
||||
@@ -493,7 +493,7 @@ class OpenEndedModuleXmlTest(unittest.TestCase, DummyModulestore):
|
||||
hint = "blah"
|
||||
|
||||
def setUp(self):
|
||||
self.test_system = test_system()
|
||||
self.test_system = get_test_system()
|
||||
self.test_system.xqueue['interface'] = Mock(
|
||||
send_to_queue=Mock(side_effect=[1, "queued"])
|
||||
)
|
||||
@@ -569,6 +569,7 @@ class OpenEndedModuleXmlTest(unittest.TestCase, DummyModulestore):
|
||||
#Mock a student submitting an assessment
|
||||
assessment_dict = MockQueryDict()
|
||||
assessment_dict.update({'assessment': sum(assessment), 'score_list[]': assessment})
|
||||
#from nose.tools import set_trace; set_trace()
|
||||
module.handle_ajax("save_assessment", assessment_dict)
|
||||
task_one_json = json.loads(module.task_states[0])
|
||||
self.assertEqual(json.loads(task_one_json['child_history'][0]['post_assessment']), assessment)
|
||||
|
||||
@@ -15,12 +15,12 @@ from xmodule.tests.test_export import DATA_DIR
|
||||
ORG = 'test_org'
|
||||
COURSE = 'conditional' # name of directory with course data
|
||||
|
||||
from . import test_system
|
||||
from . import get_test_system
|
||||
|
||||
|
||||
class DummySystem(ImportSystem):
|
||||
|
||||
@patch('xmodule.modulestore.xml.OSFS', lambda dir: MemoryFS())
|
||||
@patch('xmodule.modulestore.xml.OSFS', lambda directory: MemoryFS())
|
||||
def __init__(self, load_error_modules):
|
||||
|
||||
xmlstore = XMLModuleStore("data_dir", course_dirs=[], load_error_modules=load_error_modules)
|
||||
@@ -41,7 +41,8 @@ class DummySystem(ImportSystem):
|
||||
)
|
||||
|
||||
def render_template(self, template, context):
|
||||
raise Exception("Shouldn't be called")
|
||||
raise Exception("Shouldn't be called")
|
||||
|
||||
|
||||
class ConditionalFactory(object):
|
||||
"""
|
||||
@@ -93,7 +94,7 @@ class ConditionalFactory(object):
|
||||
# return dict:
|
||||
return {'cond_module': cond_module,
|
||||
'source_module': source_module,
|
||||
'child_module': child_module }
|
||||
'child_module': child_module}
|
||||
|
||||
|
||||
class ConditionalModuleBasicTest(unittest.TestCase):
|
||||
@@ -103,21 +104,20 @@ class ConditionalModuleBasicTest(unittest.TestCase):
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
self.test_system = test_system()
|
||||
self.test_system = get_test_system()
|
||||
|
||||
def test_icon_class(self):
|
||||
'''verify that get_icon_class works independent of condition satisfaction'''
|
||||
modules = ConditionalFactory.create(self.test_system)
|
||||
for attempted in ["false", "true"]:
|
||||
for icon_class in [ 'other', 'problem', 'video']:
|
||||
for icon_class in ['other', 'problem', 'video']:
|
||||
modules['source_module'].is_attempted = attempted
|
||||
modules['child_module'].get_icon_class = lambda: icon_class
|
||||
self.assertEqual(modules['cond_module'].get_icon_class(), icon_class)
|
||||
|
||||
|
||||
def test_get_html(self):
|
||||
modules = ConditionalFactory.create(self.test_system)
|
||||
# because test_system returns the repr of the context dict passed to render_template,
|
||||
# because get_test_system returns the repr of the context dict passed to render_template,
|
||||
# we reverse it here
|
||||
html = modules['cond_module'].get_html()
|
||||
html_dict = literal_eval(html)
|
||||
@@ -161,7 +161,7 @@ class ConditionalModuleXmlTest(unittest.TestCase):
|
||||
return DummySystem(load_error_modules)
|
||||
|
||||
def setUp(self):
|
||||
self.test_system = test_system()
|
||||
self.test_system = get_test_system()
|
||||
|
||||
def get_course(self, name):
|
||||
"""Get a test course by directory name. If there's more than one, error."""
|
||||
@@ -224,4 +224,3 @@ class ConditionalModuleXmlTest(unittest.TestCase):
|
||||
print "post-attempt ajax: ", ajax
|
||||
html = ajax['html']
|
||||
self.assertTrue(any(['This is a secret' in item for item in html]))
|
||||
|
||||
|
||||
@@ -2,25 +2,30 @@
|
||||
Tests for ErrorModule and NonStaffErrorModule
|
||||
"""
|
||||
import unittest
|
||||
from xmodule.tests import test_system
|
||||
from xmodule.tests import get_test_system
|
||||
import xmodule.error_module as error_module
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.x_module import XModuleDescriptor
|
||||
from mock import MagicMock
|
||||
|
||||
|
||||
class TestErrorModule(unittest.TestCase):
|
||||
"""
|
||||
Tests for ErrorModule and ErrorDescriptor
|
||||
"""
|
||||
class SetupTestErrorModules():
|
||||
def setUp(self):
|
||||
self.system = test_system()
|
||||
self.system = get_test_system()
|
||||
self.org = "org"
|
||||
self.course = "course"
|
||||
self.location = Location(['i4x', self.org, self.course, None, None])
|
||||
self.valid_xml = u"<problem>ABC \N{SNOWMAN}</problem>"
|
||||
self.error_msg = "Error"
|
||||
|
||||
|
||||
class TestErrorModule(unittest.TestCase, SetupTestErrorModules):
|
||||
"""
|
||||
Tests for ErrorModule and ErrorDescriptor
|
||||
"""
|
||||
def setUp(self):
|
||||
SetupTestErrorModules.setUp(self)
|
||||
|
||||
def test_error_module_xml_rendering(self):
|
||||
descriptor = error_module.ErrorDescriptor.from_xml(
|
||||
self.valid_xml, self.system, self.org, self.course, self.error_msg)
|
||||
@@ -45,10 +50,12 @@ class TestErrorModule(unittest.TestCase):
|
||||
self.assertIn(repr(descriptor), context_repr)
|
||||
|
||||
|
||||
class TestNonStaffErrorModule(TestErrorModule):
|
||||
class TestNonStaffErrorModule(unittest.TestCase, SetupTestErrorModules):
|
||||
"""
|
||||
Tests for NonStaffErrorModule and NonStaffErrorDescriptor
|
||||
"""
|
||||
def setUp(self):
|
||||
SetupTestErrorModules.setUp(self)
|
||||
|
||||
def test_non_staff_error_module_create(self):
|
||||
descriptor = error_module.NonStaffErrorDescriptor.from_xml(
|
||||
|
||||
@@ -5,7 +5,7 @@ from mock import Mock
|
||||
from xmodule.html_module import HtmlModule
|
||||
from xmodule.modulestore import Location
|
||||
|
||||
from . import test_system
|
||||
from . import get_test_system
|
||||
|
||||
class HtmlModuleSubstitutionTestCase(unittest.TestCase):
|
||||
descriptor = Mock()
|
||||
@@ -13,7 +13,7 @@ class HtmlModuleSubstitutionTestCase(unittest.TestCase):
|
||||
def test_substitution_works(self):
|
||||
sample_xml = '''%%USER_ID%%'''
|
||||
module_data = {'data': sample_xml}
|
||||
module_system = test_system()
|
||||
module_system = get_test_system()
|
||||
module = HtmlModule(module_system, self.descriptor, module_data)
|
||||
self.assertEqual(module.get_html(), str(module_system.anonymous_student_id))
|
||||
|
||||
@@ -25,14 +25,14 @@ class HtmlModuleSubstitutionTestCase(unittest.TestCase):
|
||||
</html>
|
||||
'''
|
||||
module_data = {'data': sample_xml}
|
||||
module = HtmlModule(test_system(), self.descriptor, module_data)
|
||||
module = HtmlModule(get_test_system(), self.descriptor, module_data)
|
||||
self.assertEqual(module.get_html(), sample_xml)
|
||||
|
||||
|
||||
def test_substitution_without_anonymous_student_id(self):
|
||||
sample_xml = '''%%USER_ID%%'''
|
||||
module_data = {'data': sample_xml}
|
||||
module_system = test_system()
|
||||
module_system = get_test_system()
|
||||
module_system.anonymous_student_id = None
|
||||
module = HtmlModule(module_system, self.descriptor, module_data)
|
||||
self.assertEqual(module.get_html(), sample_xml)
|
||||
|
||||
@@ -8,7 +8,7 @@ import unittest
|
||||
from xmodule.poll_module import PollDescriptor
|
||||
from xmodule.conditional_module import ConditionalDescriptor
|
||||
from xmodule.word_cloud_module import WordCloudDescriptor
|
||||
from xmodule.tests import test_system
|
||||
from xmodule.tests import get_test_system
|
||||
|
||||
class PostData:
|
||||
"""Class which emulate postdata."""
|
||||
@@ -30,7 +30,7 @@ class LogicTest(unittest.TestCase):
|
||||
"""Empty object."""
|
||||
pass
|
||||
|
||||
self.system = test_system()
|
||||
self.system = get_test_system()
|
||||
self.descriptor = EmptyClass()
|
||||
|
||||
self.xmodule_class = self.descriptor_class.module_class
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import unittest
|
||||
from xmodule.modulestore import Location
|
||||
from .import test_system
|
||||
from .import get_test_system
|
||||
from test_util_open_ended import MockQueryDict, DummyModulestore
|
||||
import json
|
||||
|
||||
@@ -39,7 +39,7 @@ class PeerGradingModuleTest(unittest.TestCase, DummyModulestore):
|
||||
Create a peer grading module from a test system
|
||||
@return:
|
||||
"""
|
||||
self.test_system = test_system()
|
||||
self.test_system = get_test_system()
|
||||
self.test_system.open_ended_grading_interface = None
|
||||
self.setup_modulestore(COURSE)
|
||||
self.peer_grading = self.get_module_from_location(self.problem_location, COURSE)
|
||||
@@ -151,10 +151,10 @@ class PeerGradingModuleScoredTest(unittest.TestCase, DummyModulestore):
|
||||
Create a peer grading module from a test system
|
||||
@return:
|
||||
"""
|
||||
self.test_system = test_system()
|
||||
self.test_system = get_test_system()
|
||||
self.test_system.open_ended_grading_interface = None
|
||||
self.setup_modulestore(COURSE)
|
||||
|
||||
def test_metadata_load(self):
|
||||
peer_grading = self.get_module_from_location(self.problem_location, COURSE)
|
||||
self.assertEqual(peer_grading.closed(), False)
|
||||
self.assertEqual(peer_grading.closed(), False)
|
||||
|
||||
@@ -5,7 +5,7 @@ import unittest
|
||||
from xmodule.progress import Progress
|
||||
from xmodule import x_module
|
||||
|
||||
from . import test_system
|
||||
from . import get_test_system
|
||||
|
||||
|
||||
class ProgressTest(unittest.TestCase):
|
||||
@@ -134,6 +134,6 @@ class ModuleProgressTest(unittest.TestCase):
|
||||
'''
|
||||
def test_xmodule_default(self):
|
||||
'''Make sure default get_progress exists, returns None'''
|
||||
xm = x_module.XModule(test_system(), None, {'location': 'a://b/c/d/e'})
|
||||
xm = x_module.XModule(get_test_system(), None, {'location': 'a://b/c/d/e'})
|
||||
p = xm.get_progress()
|
||||
self.assertEqual(p, None)
|
||||
|
||||
@@ -6,7 +6,7 @@ from xmodule.open_ended_grading_classes.self_assessment_module import SelfAssess
|
||||
from xmodule.modulestore import Location
|
||||
from lxml import etree
|
||||
|
||||
from . import test_system
|
||||
from . import get_test_system
|
||||
|
||||
import test_util_open_ended
|
||||
|
||||
@@ -51,7 +51,7 @@ class SelfAssessmentTest(unittest.TestCase):
|
||||
'skip_basic_checks': False,
|
||||
}
|
||||
|
||||
self.module = SelfAssessmentModule(test_system(), self.location,
|
||||
self.module = SelfAssessmentModule(get_test_system(), self.location,
|
||||
self.definition,
|
||||
self.descriptor,
|
||||
static_data)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from .import test_system
|
||||
from .import get_test_system
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.modulestore.xml import ImportSystem, XMLModuleStore
|
||||
from xmodule.tests.test_export import DATA_DIR
|
||||
@@ -37,7 +37,7 @@ class DummyModulestore(object):
|
||||
"""
|
||||
A mixin that allows test classes to have convenience functions to get a module given a location
|
||||
"""
|
||||
test_system = test_system()
|
||||
get_test_system = get_test_system()
|
||||
|
||||
def setup_modulestore(self, name):
|
||||
self.modulestore = XMLModuleStore(DATA_DIR, course_dirs=[name])
|
||||
|
||||
@@ -20,7 +20,7 @@ from lxml import etree
|
||||
|
||||
from xmodule.video_module import VideoDescriptor, VideoModule
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.tests import test_system
|
||||
from xmodule.tests import get_test_system
|
||||
from xmodule.tests.test_logic import LogicTest
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ class VideoFactory(object):
|
||||
|
||||
descriptor = Mock(weight="1")
|
||||
|
||||
system = test_system()
|
||||
system = get_test_system()
|
||||
system.render_template = lambda template, context: context
|
||||
module = VideoModule(system, descriptor, model_data)
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ from xblock.core import Scope, String, Dict, Boolean, Integer, Float, Any, List
|
||||
from xmodule.fields import Date, Timedelta
|
||||
from xmodule.xml_module import XmlDescriptor, serialize_field, deserialize_field
|
||||
import unittest
|
||||
from .import test_system
|
||||
from .import get_test_system
|
||||
from nose.tools import assert_equals
|
||||
from mock import Mock
|
||||
|
||||
@@ -140,7 +140,7 @@ class EditableMetadataFieldsTest(unittest.TestCase):
|
||||
|
||||
# Start of helper methods
|
||||
def get_xml_editable_fields(self, model_data):
|
||||
system = test_system()
|
||||
system = get_test_system()
|
||||
system.render_template = Mock(return_value="<div>Test Template HTML</div>")
|
||||
return XmlDescriptor(runtime=system, model_data=model_data).editable_metadata_fields
|
||||
|
||||
@@ -152,7 +152,7 @@ class EditableMetadataFieldsTest(unittest.TestCase):
|
||||
non_editable_fields.append(TestModuleDescriptor.due)
|
||||
return non_editable_fields
|
||||
|
||||
system = test_system()
|
||||
system = get_test_system()
|
||||
system.render_template = Mock(return_value="<div>Test Template HTML</div>")
|
||||
return TestModuleDescriptor(runtime=system, model_data=model_data)
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ from xblock.core import XBlock, Scope, String, Integer, Float, ModelType
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def dummy_track(event_type, event):
|
||||
def dummy_track(_event_type, _event):
|
||||
pass
|
||||
|
||||
|
||||
@@ -231,7 +231,7 @@ class XModule(XModuleFields, HTMLSnippet, XBlock):
|
||||
'''
|
||||
return self.icon_class
|
||||
|
||||
### Functions used in the LMS
|
||||
# Functions used in the LMS
|
||||
|
||||
def get_score(self):
|
||||
"""
|
||||
@@ -272,7 +272,7 @@ class XModule(XModuleFields, HTMLSnippet, XBlock):
|
||||
'''
|
||||
return None
|
||||
|
||||
def handle_ajax(self, dispatch, get):
|
||||
def handle_ajax(self, _dispatch, _get):
|
||||
''' dispatch is last part of the URL.
|
||||
get is a dictionary-like object '''
|
||||
return ""
|
||||
@@ -647,13 +647,13 @@ class XModuleDescriptor(XModuleFields, HTMLSnippet, ResourceTemplates, XBlock):
|
||||
# 1. A select editor for fields with a list of possible values (includes Booleans).
|
||||
# 2. Number editors for integers and floats.
|
||||
# 3. A generic string editor for anything else (editing JSON representation of the value).
|
||||
type = "Generic"
|
||||
editor_type = "Generic"
|
||||
values = [] if field.values is None else copy.deepcopy(field.values)
|
||||
if isinstance(values, tuple):
|
||||
values = list(values)
|
||||
if isinstance(values, list):
|
||||
if len(values) > 0:
|
||||
type = "Select"
|
||||
editor_type = "Select"
|
||||
for index, choice in enumerate(values):
|
||||
json_choice = copy.deepcopy(choice)
|
||||
if isinstance(json_choice, dict) and 'value' in json_choice:
|
||||
@@ -662,11 +662,11 @@ class XModuleDescriptor(XModuleFields, HTMLSnippet, ResourceTemplates, XBlock):
|
||||
json_choice = field.to_json(json_choice)
|
||||
values[index] = json_choice
|
||||
elif isinstance(field, Integer):
|
||||
type = "Integer"
|
||||
editor_type = "Integer"
|
||||
elif isinstance(field, Float):
|
||||
type = "Float"
|
||||
editor_type = "Float"
|
||||
metadata_fields[field.name] = {'field_name': field.name,
|
||||
'type': type,
|
||||
'type': editor_type,
|
||||
'display_name': field.display_name,
|
||||
'value': field.to_json(value),
|
||||
'options': values,
|
||||
@@ -862,7 +862,7 @@ class ModuleSystem(object):
|
||||
|
||||
class DoNothingCache(object):
|
||||
"""A duck-compatible object to use in ModuleSystem when there's no cache."""
|
||||
def get(self, key):
|
||||
def get(self, _key):
|
||||
return None
|
||||
|
||||
def set(self, key, value, timeout=None):
|
||||
|
||||
@@ -56,7 +56,6 @@ def get_metadata_from_xml(xml_object, remove=True):
|
||||
if meta is None:
|
||||
return ''
|
||||
dmdata = meta.text
|
||||
#log.debug('meta for %s loaded: %s' % (xml_object,dmdata))
|
||||
if remove:
|
||||
xml_object.remove(meta)
|
||||
return dmdata
|
||||
|
||||
@@ -3,6 +3,11 @@ describe 'Logger', ->
|
||||
expect(window.log_event).toBe Logger.log
|
||||
|
||||
describe 'log', ->
|
||||
it 'sends an event to Segment.io, if the event is whitelisted', ->
|
||||
spyOn(analytics, 'track')
|
||||
Logger.log 'seq_goto', 'data'
|
||||
expect(analytics.track).toHaveBeenCalledWith 'seq_goto', 'data'
|
||||
|
||||
it 'send a request to log event', ->
|
||||
spyOn $, 'getWithPrefix'
|
||||
Logger.log 'example', 'data'
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
class @Logger
|
||||
# events we want sent to Segment.io for tracking
|
||||
SEGMENT_IO_WHITELIST = ["seq_goto", "seq_next", "seq_prev"]
|
||||
|
||||
@log: (event_type, data) ->
|
||||
if event_type in SEGMENT_IO_WHITELIST
|
||||
# Segment.io event tracking
|
||||
analytics.track event_type, data
|
||||
|
||||
$.getWithPrefix '/event',
|
||||
event_type: event_type
|
||||
event: JSON.stringify(data)
|
||||
|
||||
5538
common/static/js/vendor/analytics.js
vendored
Normal file
5538
common/static/js/vendor/analytics.js
vendored
Normal file
@@ -0,0 +1,5538 @@
|
||||
;(function(){
|
||||
|
||||
/**
|
||||
* Require the given path.
|
||||
*
|
||||
* @param {String} path
|
||||
* @return {Object} exports
|
||||
* @api public
|
||||
*/
|
||||
|
||||
function require(path, parent, orig) {
|
||||
var resolved = require.resolve(path);
|
||||
|
||||
// lookup failed
|
||||
if (null == resolved) {
|
||||
orig = orig || path;
|
||||
parent = parent || 'root';
|
||||
var err = new Error('Failed to require "' + orig + '" from "' + parent + '"');
|
||||
err.path = orig;
|
||||
err.parent = parent;
|
||||
err.require = true;
|
||||
throw err;
|
||||
}
|
||||
|
||||
var module = require.modules[resolved];
|
||||
|
||||
// perform real require()
|
||||
// by invoking the module's
|
||||
// registered function
|
||||
if (!module.exports) {
|
||||
module.exports = {};
|
||||
module.client = module.component = true;
|
||||
module.call(this, module.exports, require.relative(resolved), module);
|
||||
}
|
||||
|
||||
return module.exports;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registered modules.
|
||||
*/
|
||||
|
||||
require.modules = {};
|
||||
|
||||
/**
|
||||
* Registered aliases.
|
||||
*/
|
||||
|
||||
require.aliases = {};
|
||||
|
||||
/**
|
||||
* Resolve `path`.
|
||||
*
|
||||
* Lookup:
|
||||
*
|
||||
* - PATH/index.js
|
||||
* - PATH.js
|
||||
* - PATH
|
||||
*
|
||||
* @param {String} path
|
||||
* @return {String} path or null
|
||||
* @api private
|
||||
*/
|
||||
|
||||
require.resolve = function(path) {
|
||||
if (path.charAt(0) === '/') path = path.slice(1);
|
||||
|
||||
var paths = [
|
||||
path,
|
||||
path + '.js',
|
||||
path + '.json',
|
||||
path + '/index.js',
|
||||
path + '/index.json'
|
||||
];
|
||||
|
||||
for (var i = 0; i < paths.length; i++) {
|
||||
var path = paths[i];
|
||||
if (require.modules.hasOwnProperty(path)) return path;
|
||||
if (require.aliases.hasOwnProperty(path)) return require.aliases[path];
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Normalize `path` relative to the current path.
|
||||
*
|
||||
* @param {String} curr
|
||||
* @param {String} path
|
||||
* @return {String}
|
||||
* @api private
|
||||
*/
|
||||
|
||||
require.normalize = function(curr, path) {
|
||||
var segs = [];
|
||||
|
||||
if ('.' != path.charAt(0)) return path;
|
||||
|
||||
curr = curr.split('/');
|
||||
path = path.split('/');
|
||||
|
||||
for (var i = 0; i < path.length; ++i) {
|
||||
if ('..' == path[i]) {
|
||||
curr.pop();
|
||||
} else if ('.' != path[i] && '' != path[i]) {
|
||||
segs.push(path[i]);
|
||||
}
|
||||
}
|
||||
|
||||
return curr.concat(segs).join('/');
|
||||
};
|
||||
|
||||
/**
|
||||
* Register module at `path` with callback `definition`.
|
||||
*
|
||||
* @param {String} path
|
||||
* @param {Function} definition
|
||||
* @api private
|
||||
*/
|
||||
|
||||
require.register = function(path, definition) {
|
||||
require.modules[path] = definition;
|
||||
};
|
||||
|
||||
/**
|
||||
* Alias a module definition.
|
||||
*
|
||||
* @param {String} from
|
||||
* @param {String} to
|
||||
* @api private
|
||||
*/
|
||||
|
||||
require.alias = function(from, to) {
|
||||
if (!require.modules.hasOwnProperty(from)) {
|
||||
throw new Error('Failed to alias "' + from + '", it does not exist');
|
||||
}
|
||||
require.aliases[to] = from;
|
||||
};
|
||||
|
||||
/**
|
||||
* Return a require function relative to the `parent` path.
|
||||
*
|
||||
* @param {String} parent
|
||||
* @return {Function}
|
||||
* @api private
|
||||
*/
|
||||
|
||||
require.relative = function(parent) {
|
||||
var p = require.normalize(parent, '..');
|
||||
|
||||
/**
|
||||
* lastIndexOf helper.
|
||||
*/
|
||||
|
||||
function lastIndexOf(arr, obj) {
|
||||
var i = arr.length;
|
||||
while (i--) {
|
||||
if (arr[i] === obj) return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* The relative require() itself.
|
||||
*/
|
||||
|
||||
function localRequire(path) {
|
||||
var resolved = localRequire.resolve(path);
|
||||
return require(resolved, parent, path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve relative to the parent.
|
||||
*/
|
||||
|
||||
localRequire.resolve = function(path) {
|
||||
var c = path.charAt(0);
|
||||
if ('/' == c) return path.slice(1);
|
||||
if ('.' == c) return require.normalize(p, path);
|
||||
|
||||
// resolve deps by returning
|
||||
// the dep in the nearest "deps"
|
||||
// directory
|
||||
var segs = parent.split('/');
|
||||
var i = lastIndexOf(segs, 'deps') + 1;
|
||||
if (!i) i = 0;
|
||||
path = segs.slice(0, i + 1).join('/') + '/deps/' + path;
|
||||
return path;
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if module is defined at `path`.
|
||||
*/
|
||||
|
||||
localRequire.exists = function(path) {
|
||||
return require.modules.hasOwnProperty(localRequire.resolve(path));
|
||||
};
|
||||
|
||||
return localRequire;
|
||||
};
|
||||
require.register("avetisk-defaults/index.js", function(exports, require, module){
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Merge default values.
|
||||
*
|
||||
* @param {Object} dest
|
||||
* @param {Object} defaults
|
||||
* @return {Object}
|
||||
* @api public
|
||||
*/
|
||||
var defaults = function (dest, src, recursive) {
|
||||
for (var prop in src) {
|
||||
if (recursive && dest[prop] instanceof Object && src[prop] instanceof Object) {
|
||||
dest[prop] = defaults(dest[prop], src[prop], true);
|
||||
} else if (! (prop in dest)) {
|
||||
dest[prop] = src[prop];
|
||||
}
|
||||
}
|
||||
|
||||
return dest;
|
||||
};
|
||||
|
||||
/**
|
||||
* Expose `defaults`.
|
||||
*/
|
||||
module.exports = defaults;
|
||||
|
||||
});
|
||||
require.register("component-clone/index.js", function(exports, require, module){
|
||||
|
||||
/**
|
||||
* Module dependencies.
|
||||
*/
|
||||
|
||||
var type;
|
||||
|
||||
try {
|
||||
type = require('type');
|
||||
} catch(e){
|
||||
type = require('type-component');
|
||||
}
|
||||
|
||||
/**
|
||||
* Module exports.
|
||||
*/
|
||||
|
||||
module.exports = clone;
|
||||
|
||||
/**
|
||||
* Clones objects.
|
||||
*
|
||||
* @param {Mixed} any object
|
||||
* @api public
|
||||
*/
|
||||
|
||||
function clone(obj){
|
||||
switch (type(obj)) {
|
||||
case 'object':
|
||||
var copy = {};
|
||||
for (var key in obj) {
|
||||
if (obj.hasOwnProperty(key)) {
|
||||
copy[key] = clone(obj[key]);
|
||||
}
|
||||
}
|
||||
return copy;
|
||||
|
||||
case 'array':
|
||||
var copy = new Array(obj.length);
|
||||
for (var i = 0, l = obj.length; i < l; i++) {
|
||||
copy[i] = clone(obj[i]);
|
||||
}
|
||||
return copy;
|
||||
|
||||
case 'regexp':
|
||||
// from millermedeiros/amd-utils - MIT
|
||||
var flags = '';
|
||||
flags += obj.multiline ? 'm' : '';
|
||||
flags += obj.global ? 'g' : '';
|
||||
flags += obj.ignoreCase ? 'i' : '';
|
||||
return new RegExp(obj.source, flags);
|
||||
|
||||
case 'date':
|
||||
return new Date(obj.getTime());
|
||||
|
||||
default: // string, number, boolean, …
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
require.register("component-cookie/index.js", function(exports, require, module){
|
||||
/**
|
||||
* Encode.
|
||||
*/
|
||||
|
||||
var encode = encodeURIComponent;
|
||||
|
||||
/**
|
||||
* Decode.
|
||||
*/
|
||||
|
||||
var decode = decodeURIComponent;
|
||||
|
||||
/**
|
||||
* Set or get cookie `name` with `value` and `options` object.
|
||||
*
|
||||
* @param {String} name
|
||||
* @param {String} value
|
||||
* @param {Object} options
|
||||
* @return {Mixed}
|
||||
* @api public
|
||||
*/
|
||||
|
||||
module.exports = function(name, value, options){
|
||||
switch (arguments.length) {
|
||||
case 3:
|
||||
case 2:
|
||||
return set(name, value, options);
|
||||
case 1:
|
||||
return get(name);
|
||||
default:
|
||||
return all();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Set cookie `name` to `value`.
|
||||
*
|
||||
* @param {String} name
|
||||
* @param {String} value
|
||||
* @param {Object} options
|
||||
* @api private
|
||||
*/
|
||||
|
||||
function set(name, value, options) {
|
||||
options = options || {};
|
||||
var str = encode(name) + '=' + encode(value);
|
||||
|
||||
if (null == value) options.maxage = -1;
|
||||
|
||||
if (options.maxage) {
|
||||
options.expires = new Date(+new Date + options.maxage);
|
||||
}
|
||||
|
||||
if (options.path) str += '; path=' + options.path;
|
||||
if (options.domain) str += '; domain=' + options.domain;
|
||||
if (options.expires) str += '; expires=' + options.expires.toUTCString();
|
||||
if (options.secure) str += '; secure';
|
||||
|
||||
document.cookie = str;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all cookies.
|
||||
*
|
||||
* @return {Object}
|
||||
* @api private
|
||||
*/
|
||||
|
||||
function all() {
|
||||
return parse(document.cookie);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cookie `name`.
|
||||
*
|
||||
* @param {String} name
|
||||
* @return {String}
|
||||
* @api private
|
||||
*/
|
||||
|
||||
function get(name) {
|
||||
return all()[name];
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse cookie `str`.
|
||||
*
|
||||
* @param {String} str
|
||||
* @return {Object}
|
||||
* @api private
|
||||
*/
|
||||
|
||||
function parse(str) {
|
||||
var obj = {};
|
||||
var pairs = str.split(/ *; */);
|
||||
var pair;
|
||||
if ('' == pairs[0]) return obj;
|
||||
for (var i = 0; i < pairs.length; ++i) {
|
||||
pair = pairs[i].split('=');
|
||||
obj[decode(pair[0])] = decode(pair[1]);
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
});
|
||||
require.register("component-each/index.js", function(exports, require, module){
|
||||
|
||||
/**
|
||||
* Module dependencies.
|
||||
*/
|
||||
|
||||
var type = require('type');
|
||||
|
||||
/**
|
||||
* HOP reference.
|
||||
*/
|
||||
|
||||
var has = Object.prototype.hasOwnProperty;
|
||||
|
||||
/**
|
||||
* Iterate the given `obj` and invoke `fn(val, i)`.
|
||||
*
|
||||
* @param {String|Array|Object} obj
|
||||
* @param {Function} fn
|
||||
* @api public
|
||||
*/
|
||||
|
||||
module.exports = function(obj, fn){
|
||||
switch (type(obj)) {
|
||||
case 'array':
|
||||
return array(obj, fn);
|
||||
case 'object':
|
||||
if ('number' == typeof obj.length) return array(obj, fn);
|
||||
return object(obj, fn);
|
||||
case 'string':
|
||||
return string(obj, fn);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Iterate string chars.
|
||||
*
|
||||
* @param {String} obj
|
||||
* @param {Function} fn
|
||||
* @api private
|
||||
*/
|
||||
|
||||
function string(obj, fn) {
|
||||
for (var i = 0; i < obj.length; ++i) {
|
||||
fn(obj.charAt(i), i);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterate object keys.
|
||||
*
|
||||
* @param {Object} obj
|
||||
* @param {Function} fn
|
||||
* @api private
|
||||
*/
|
||||
|
||||
function object(obj, fn) {
|
||||
for (var key in obj) {
|
||||
if (has.call(obj, key)) {
|
||||
fn(key, obj[key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterate array-ish.
|
||||
*
|
||||
* @param {Array|Object} obj
|
||||
* @param {Function} fn
|
||||
* @api private
|
||||
*/
|
||||
|
||||
function array(obj, fn) {
|
||||
for (var i = 0; i < obj.length; ++i) {
|
||||
fn(obj[i], i);
|
||||
}
|
||||
}
|
||||
});
|
||||
require.register("component-event/index.js", function(exports, require, module){
|
||||
|
||||
/**
|
||||
* Bind `el` event `type` to `fn`.
|
||||
*
|
||||
* @param {Element} el
|
||||
* @param {String} type
|
||||
* @param {Function} fn
|
||||
* @param {Boolean} capture
|
||||
* @return {Function}
|
||||
* @api public
|
||||
*/
|
||||
|
||||
exports.bind = function(el, type, fn, capture){
|
||||
if (el.addEventListener) {
|
||||
el.addEventListener(type, fn, capture || false);
|
||||
} else {
|
||||
el.attachEvent('on' + type, fn);
|
||||
}
|
||||
return fn;
|
||||
};
|
||||
|
||||
/**
|
||||
* Unbind `el` event `type`'s callback `fn`.
|
||||
*
|
||||
* @param {Element} el
|
||||
* @param {String} type
|
||||
* @param {Function} fn
|
||||
* @param {Boolean} capture
|
||||
* @return {Function}
|
||||
* @api public
|
||||
*/
|
||||
|
||||
exports.unbind = function(el, type, fn, capture){
|
||||
if (el.removeEventListener) {
|
||||
el.removeEventListener(type, fn, capture || false);
|
||||
} else {
|
||||
el.detachEvent('on' + type, fn);
|
||||
}
|
||||
return fn;
|
||||
};
|
||||
|
||||
});
|
||||
require.register("component-inherit/index.js", function(exports, require, module){
|
||||
|
||||
module.exports = function(a, b){
|
||||
var fn = function(){};
|
||||
fn.prototype = b.prototype;
|
||||
a.prototype = new fn;
|
||||
a.prototype.constructor = a;
|
||||
};
|
||||
});
|
||||
require.register("component-object/index.js", function(exports, require, module){
|
||||
|
||||
/**
|
||||
* HOP ref.
|
||||
*/
|
||||
|
||||
var has = Object.prototype.hasOwnProperty;
|
||||
|
||||
/**
|
||||
* Return own keys in `obj`.
|
||||
*
|
||||
* @param {Object} obj
|
||||
* @return {Array}
|
||||
* @api public
|
||||
*/
|
||||
|
||||
exports.keys = Object.keys || function(obj){
|
||||
var keys = [];
|
||||
for (var key in obj) {
|
||||
if (has.call(obj, key)) {
|
||||
keys.push(key);
|
||||
}
|
||||
}
|
||||
return keys;
|
||||
};
|
||||
|
||||
/**
|
||||
* Return own values in `obj`.
|
||||
*
|
||||
* @param {Object} obj
|
||||
* @return {Array}
|
||||
* @api public
|
||||
*/
|
||||
|
||||
exports.values = function(obj){
|
||||
var vals = [];
|
||||
for (var key in obj) {
|
||||
if (has.call(obj, key)) {
|
||||
vals.push(obj[key]);
|
||||
}
|
||||
}
|
||||
return vals;
|
||||
};
|
||||
|
||||
/**
|
||||
* Merge `b` into `a`.
|
||||
*
|
||||
* @param {Object} a
|
||||
* @param {Object} b
|
||||
* @return {Object} a
|
||||
* @api public
|
||||
*/
|
||||
|
||||
exports.merge = function(a, b){
|
||||
for (var key in b) {
|
||||
if (has.call(b, key)) {
|
||||
a[key] = b[key];
|
||||
}
|
||||
}
|
||||
return a;
|
||||
};
|
||||
|
||||
/**
|
||||
* Return length of `obj`.
|
||||
*
|
||||
* @param {Object} obj
|
||||
* @return {Number}
|
||||
* @api public
|
||||
*/
|
||||
|
||||
exports.length = function(obj){
|
||||
return exports.keys(obj).length;
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if `obj` is empty.
|
||||
*
|
||||
* @param {Object} obj
|
||||
* @return {Boolean}
|
||||
* @api public
|
||||
*/
|
||||
|
||||
exports.isEmpty = function(obj){
|
||||
return 0 == exports.length(obj);
|
||||
};
|
||||
});
|
||||
require.register("component-trim/index.js", function(exports, require, module){
|
||||
|
||||
exports = module.exports = trim;
|
||||
|
||||
function trim(str){
|
||||
return str.replace(/^\s*|\s*$/g, '');
|
||||
}
|
||||
|
||||
exports.left = function(str){
|
||||
return str.replace(/^\s*/, '');
|
||||
};
|
||||
|
||||
exports.right = function(str){
|
||||
return str.replace(/\s*$/, '');
|
||||
};
|
||||
|
||||
});
|
||||
require.register("component-querystring/index.js", function(exports, require, module){
|
||||
|
||||
/**
|
||||
* Module dependencies.
|
||||
*/
|
||||
|
||||
var trim = require('trim');
|
||||
|
||||
/**
|
||||
* Parse the given query `str`.
|
||||
*
|
||||
* @param {String} str
|
||||
* @return {Object}
|
||||
* @api public
|
||||
*/
|
||||
|
||||
exports.parse = function(str){
|
||||
if ('string' != typeof str) return {};
|
||||
|
||||
str = trim(str);
|
||||
if ('' == str) return {};
|
||||
|
||||
var obj = {};
|
||||
var pairs = str.split('&');
|
||||
for (var i = 0; i < pairs.length; i++) {
|
||||
var parts = pairs[i].split('=');
|
||||
obj[parts[0]] = null == parts[1]
|
||||
? ''
|
||||
: decodeURIComponent(parts[1]);
|
||||
}
|
||||
|
||||
return obj;
|
||||
};
|
||||
|
||||
/**
|
||||
* Stringify the given `obj`.
|
||||
*
|
||||
* @param {Object} obj
|
||||
* @return {String}
|
||||
* @api public
|
||||
*/
|
||||
|
||||
exports.stringify = function(obj){
|
||||
if (!obj) return '';
|
||||
var pairs = [];
|
||||
for (var key in obj) {
|
||||
pairs.push(encodeURIComponent(key) + '=' + encodeURIComponent(obj[key]));
|
||||
}
|
||||
return pairs.join('&');
|
||||
};
|
||||
|
||||
});
|
||||
require.register("component-type/index.js", function(exports, require, module){
|
||||
|
||||
/**
|
||||
* toString ref.
|
||||
*/
|
||||
|
||||
var toString = Object.prototype.toString;
|
||||
|
||||
/**
|
||||
* Return the type of `val`.
|
||||
*
|
||||
* @param {Mixed} val
|
||||
* @return {String}
|
||||
* @api public
|
||||
*/
|
||||
|
||||
module.exports = function(val){
|
||||
switch (toString.call(val)) {
|
||||
case '[object Function]': return 'function';
|
||||
case '[object Date]': return 'date';
|
||||
case '[object RegExp]': return 'regexp';
|
||||
case '[object Arguments]': return 'arguments';
|
||||
case '[object Array]': return 'array';
|
||||
case '[object String]': return 'string';
|
||||
}
|
||||
|
||||
if (val === null) return 'null';
|
||||
if (val === undefined) return 'undefined';
|
||||
if (val && val.nodeType === 1) return 'element';
|
||||
if (val === Object(val)) return 'object';
|
||||
|
||||
return typeof val;
|
||||
};
|
||||
|
||||
});
|
||||
require.register("component-url/index.js", function(exports, require, module){
|
||||
|
||||
/**
|
||||
* Parse the given `url`.
|
||||
*
|
||||
* @param {String} str
|
||||
* @return {Object}
|
||||
* @api public
|
||||
*/
|
||||
|
||||
exports.parse = function(url){
|
||||
var a = document.createElement('a');
|
||||
a.href = url;
|
||||
return {
|
||||
href: a.href,
|
||||
host: a.host || location.host,
|
||||
port: ('0' === a.port || '' === a.port) ? location.port : a.port,
|
||||
hash: a.hash,
|
||||
hostname: a.hostname || location.hostname,
|
||||
pathname: a.pathname.charAt(0) != '/' ? '/' + a.pathname : a.pathname,
|
||||
protocol: !a.protocol || ':' == a.protocol ? location.protocol : a.protocol,
|
||||
search: a.search,
|
||||
query: a.search.slice(1)
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if `url` is absolute.
|
||||
*
|
||||
* @param {String} url
|
||||
* @return {Boolean}
|
||||
* @api public
|
||||
*/
|
||||
|
||||
exports.isAbsolute = function(url){
|
||||
return 0 == url.indexOf('//') || !!~url.indexOf('://');
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if `url` is relative.
|
||||
*
|
||||
* @param {String} url
|
||||
* @return {Boolean}
|
||||
* @api public
|
||||
*/
|
||||
|
||||
exports.isRelative = function(url){
|
||||
return !exports.isAbsolute(url);
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if `url` is cross domain.
|
||||
*
|
||||
* @param {String} url
|
||||
* @return {Boolean}
|
||||
* @api public
|
||||
*/
|
||||
|
||||
exports.isCrossDomain = function(url){
|
||||
url = exports.parse(url);
|
||||
return url.hostname !== location.hostname
|
||||
|| url.port !== location.port
|
||||
|| url.protocol !== location.protocol;
|
||||
};
|
||||
});
|
||||
require.register("segmentio-after/index.js", function(exports, require, module){
|
||||
|
||||
module.exports = function after (times, func) {
|
||||
// After 0, really?
|
||||
if (times <= 0) return func();
|
||||
|
||||
// That's more like it.
|
||||
return function() {
|
||||
if (--times < 1) {
|
||||
return func.apply(this, arguments);
|
||||
}
|
||||
};
|
||||
};
|
||||
});
|
||||
require.register("segmentio-alias/index.js", function(exports, require, module){
|
||||
|
||||
module.exports = function alias (object, aliases) {
|
||||
// For each of our aliases, rename our object's keys.
|
||||
for (var oldKey in aliases) {
|
||||
var newKey = aliases[oldKey];
|
||||
if (object[oldKey] !== undefined) {
|
||||
object[newKey] = object[oldKey];
|
||||
delete object[oldKey];
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
require.register("component-bind/index.js", function(exports, require, module){
|
||||
|
||||
/**
|
||||
* Slice reference.
|
||||
*/
|
||||
|
||||
var slice = [].slice;
|
||||
|
||||
/**
|
||||
* Bind `obj` to `fn`.
|
||||
*
|
||||
* @param {Object} obj
|
||||
* @param {Function|String} fn or string
|
||||
* @return {Function}
|
||||
* @api public
|
||||
*/
|
||||
|
||||
module.exports = function(obj, fn){
|
||||
if ('string' == typeof fn) fn = obj[fn];
|
||||
if ('function' != typeof fn) throw new Error('bind() requires a function');
|
||||
var args = [].slice.call(arguments, 2);
|
||||
return function(){
|
||||
return fn.apply(obj, args.concat(slice.call(arguments)));
|
||||
}
|
||||
};
|
||||
|
||||
});
|
||||
require.register("segmentio-bind-all/index.js", function(exports, require, module){
|
||||
|
||||
var bind = require('bind')
|
||||
, type = require('type');
|
||||
|
||||
|
||||
module.exports = function (obj) {
|
||||
for (var key in obj) {
|
||||
var val = obj[key];
|
||||
if (type(val) === 'function') obj[key] = bind(obj, obj[key]);
|
||||
}
|
||||
return obj;
|
||||
};
|
||||
});
|
||||
require.register("segmentio-canonical/index.js", function(exports, require, module){
|
||||
module.exports = function canonical () {
|
||||
var tags = document.getElementsByTagName('link');
|
||||
for (var i = 0, tag; tag = tags[i]; i++) {
|
||||
if ('canonical' == tag.getAttribute('rel')) return tag.getAttribute('href');
|
||||
}
|
||||
};
|
||||
});
|
||||
require.register("segmentio-extend/index.js", function(exports, require, module){
|
||||
|
||||
module.exports = function extend (object) {
|
||||
// Takes an unlimited number of extenders.
|
||||
var args = Array.prototype.slice.call(arguments, 1);
|
||||
|
||||
// For each extender, copy their properties on our object.
|
||||
for (var i = 0, source; source = args[i]; i++) {
|
||||
if (!source) continue;
|
||||
for (var property in source) {
|
||||
object[property] = source[property];
|
||||
}
|
||||
}
|
||||
|
||||
return object;
|
||||
};
|
||||
});
|
||||
require.register("segmentio-is-email/index.js", function(exports, require, module){
|
||||
|
||||
module.exports = function isEmail (string) {
|
||||
return (/.+\@.+\..+/).test(string);
|
||||
};
|
||||
});
|
||||
require.register("segmentio-is-meta/index.js", function(exports, require, module){
|
||||
module.exports = function isMeta (e) {
|
||||
if (e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) return true;
|
||||
|
||||
// Logic that handles checks for the middle mouse button, based
|
||||
// on [jQuery](https://github.com/jquery/jquery/blob/master/src/event.js#L466).
|
||||
var which = e.which, button = e.button;
|
||||
if (!which && button !== undefined) {
|
||||
return (!button & 1) && (!button & 2) && (button & 4);
|
||||
} else if (which === 2) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
});
|
||||
require.register("component-json-fallback/index.js", function(exports, require, module){
|
||||
/*
|
||||
json2.js
|
||||
2011-10-19
|
||||
|
||||
Public Domain.
|
||||
|
||||
NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK.
|
||||
|
||||
See http://www.JSON.org/js.html
|
||||
|
||||
|
||||
This code should be minified before deployment.
|
||||
See http://javascript.crockford.com/jsmin.html
|
||||
|
||||
USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO
|
||||
NOT CONTROL.
|
||||
|
||||
|
||||
This file creates a global JSON object containing two methods: stringify
|
||||
and parse.
|
||||
|
||||
JSON.stringify(value, replacer, space)
|
||||
value any JavaScript value, usually an object or array.
|
||||
|
||||
replacer an optional parameter that determines how object
|
||||
values are stringified for objects. It can be a
|
||||
function or an array of strings.
|
||||
|
||||
space an optional parameter that specifies the indentation
|
||||
of nested structures. If it is omitted, the text will
|
||||
be packed without extra whitespace. If it is a number,
|
||||
it will specify the number of spaces to indent at each
|
||||
level. If it is a string (such as '\t' or ' '),
|
||||
it contains the characters used to indent at each level.
|
||||
|
||||
This method produces a JSON text from a JavaScript value.
|
||||
|
||||
When an object value is found, if the object contains a toJSON
|
||||
method, its toJSON method will be called and the result will be
|
||||
stringified. A toJSON method does not serialize: it returns the
|
||||
value represented by the name/value pair that should be serialized,
|
||||
or undefined if nothing should be serialized. The toJSON method
|
||||
will be passed the key associated with the value, and this will be
|
||||
bound to the value
|
||||
|
||||
For example, this would serialize Dates as ISO strings.
|
||||
|
||||
Date.prototype.toJSON = function (key) {
|
||||
function f(n) {
|
||||
// Format integers to have at least two digits.
|
||||
return n < 10 ? '0' + n : n;
|
||||
}
|
||||
|
||||
return this.getUTCFullYear() + '-' +
|
||||
f(this.getUTCMonth() + 1) + '-' +
|
||||
f(this.getUTCDate()) + 'T' +
|
||||
f(this.getUTCHours()) + ':' +
|
||||
f(this.getUTCMinutes()) + ':' +
|
||||
f(this.getUTCSeconds()) + 'Z';
|
||||
};
|
||||
|
||||
You can provide an optional replacer method. It will be passed the
|
||||
key and value of each member, with this bound to the containing
|
||||
object. The value that is returned from your method will be
|
||||
serialized. If your method returns undefined, then the member will
|
||||
be excluded from the serialization.
|
||||
|
||||
If the replacer parameter is an array of strings, then it will be
|
||||
used to select the members to be serialized. It filters the results
|
||||
such that only members with keys listed in the replacer array are
|
||||
stringified.
|
||||
|
||||
Values that do not have JSON representations, such as undefined or
|
||||
functions, will not be serialized. Such values in objects will be
|
||||
dropped; in arrays they will be replaced with null. You can use
|
||||
a replacer function to replace those with JSON values.
|
||||
JSON.stringify(undefined) returns undefined.
|
||||
|
||||
The optional space parameter produces a stringification of the
|
||||
value that is filled with line breaks and indentation to make it
|
||||
easier to read.
|
||||
|
||||
If the space parameter is a non-empty string, then that string will
|
||||
be used for indentation. If the space parameter is a number, then
|
||||
the indentation will be that many spaces.
|
||||
|
||||
Example:
|
||||
|
||||
text = JSON.stringify(['e', {pluribus: 'unum'}]);
|
||||
// text is '["e",{"pluribus":"unum"}]'
|
||||
|
||||
|
||||
text = JSON.stringify(['e', {pluribus: 'unum'}], null, '\t');
|
||||
// text is '[\n\t"e",\n\t{\n\t\t"pluribus": "unum"\n\t}\n]'
|
||||
|
||||
text = JSON.stringify([new Date()], function (key, value) {
|
||||
return this[key] instanceof Date ?
|
||||
'Date(' + this[key] + ')' : value;
|
||||
});
|
||||
// text is '["Date(---current time---)"]'
|
||||
|
||||
|
||||
JSON.parse(text, reviver)
|
||||
This method parses a JSON text to produce an object or array.
|
||||
It can throw a SyntaxError exception.
|
||||
|
||||
The optional reviver parameter is a function that can filter and
|
||||
transform the results. It receives each of the keys and values,
|
||||
and its return value is used instead of the original value.
|
||||
If it returns what it received, then the structure is not modified.
|
||||
If it returns undefined then the member is deleted.
|
||||
|
||||
Example:
|
||||
|
||||
// Parse the text. Values that look like ISO date strings will
|
||||
// be converted to Date objects.
|
||||
|
||||
myData = JSON.parse(text, function (key, value) {
|
||||
var a;
|
||||
if (typeof value === 'string') {
|
||||
a =
|
||||
/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value);
|
||||
if (a) {
|
||||
return new Date(Date.UTC(+a[1], +a[2] - 1, +a[3], +a[4],
|
||||
+a[5], +a[6]));
|
||||
}
|
||||
}
|
||||
return value;
|
||||
});
|
||||
|
||||
myData = JSON.parse('["Date(09/09/2001)"]', function (key, value) {
|
||||
var d;
|
||||
if (typeof value === 'string' &&
|
||||
value.slice(0, 5) === 'Date(' &&
|
||||
value.slice(-1) === ')') {
|
||||
d = new Date(value.slice(5, -1));
|
||||
if (d) {
|
||||
return d;
|
||||
}
|
||||
}
|
||||
return value;
|
||||
});
|
||||
|
||||
|
||||
This is a reference implementation. You are free to copy, modify, or
|
||||
redistribute.
|
||||
*/
|
||||
|
||||
/*jslint evil: true, regexp: true */
|
||||
|
||||
/*members "", "\b", "\t", "\n", "\f", "\r", "\"", JSON, "\\", apply,
|
||||
call, charCodeAt, getUTCDate, getUTCFullYear, getUTCHours,
|
||||
getUTCMinutes, getUTCMonth, getUTCSeconds, hasOwnProperty, join,
|
||||
lastIndex, length, parse, prototype, push, replace, slice, stringify,
|
||||
test, toJSON, toString, valueOf
|
||||
*/
|
||||
|
||||
|
||||
// Create a JSON object only if one does not already exist. We create the
|
||||
// methods in a closure to avoid creating global variables.
|
||||
|
||||
var JSON = {};
|
||||
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
function f(n) {
|
||||
// Format integers to have at least two digits.
|
||||
return n < 10 ? '0' + n : n;
|
||||
}
|
||||
|
||||
if (typeof Date.prototype.toJSON !== 'function') {
|
||||
|
||||
Date.prototype.toJSON = function (key) {
|
||||
|
||||
return isFinite(this.valueOf())
|
||||
? this.getUTCFullYear() + '-' +
|
||||
f(this.getUTCMonth() + 1) + '-' +
|
||||
f(this.getUTCDate()) + 'T' +
|
||||
f(this.getUTCHours()) + ':' +
|
||||
f(this.getUTCMinutes()) + ':' +
|
||||
f(this.getUTCSeconds()) + 'Z'
|
||||
: null;
|
||||
};
|
||||
|
||||
String.prototype.toJSON =
|
||||
Number.prototype.toJSON =
|
||||
Boolean.prototype.toJSON = function (key) {
|
||||
return this.valueOf();
|
||||
};
|
||||
}
|
||||
|
||||
var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
|
||||
escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
|
||||
gap,
|
||||
indent,
|
||||
meta = { // table of character substitutions
|
||||
'\b': '\\b',
|
||||
'\t': '\\t',
|
||||
'\n': '\\n',
|
||||
'\f': '\\f',
|
||||
'\r': '\\r',
|
||||
'"' : '\\"',
|
||||
'\\': '\\\\'
|
||||
},
|
||||
rep;
|
||||
|
||||
|
||||
function quote(string) {
|
||||
|
||||
// If the string contains no control characters, no quote characters, and no
|
||||
// backslash characters, then we can safely slap some quotes around it.
|
||||
// Otherwise we must also replace the offending characters with safe escape
|
||||
// sequences.
|
||||
|
||||
escapable.lastIndex = 0;
|
||||
return escapable.test(string) ? '"' + string.replace(escapable, function (a) {
|
||||
var c = meta[a];
|
||||
return typeof c === 'string'
|
||||
? c
|
||||
: '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
|
||||
}) + '"' : '"' + string + '"';
|
||||
}
|
||||
|
||||
|
||||
function str(key, holder) {
|
||||
|
||||
// Produce a string from holder[key].
|
||||
|
||||
var i, // The loop counter.
|
||||
k, // The member key.
|
||||
v, // The member value.
|
||||
length,
|
||||
mind = gap,
|
||||
partial,
|
||||
value = holder[key];
|
||||
|
||||
// If the value has a toJSON method, call it to obtain a replacement value.
|
||||
|
||||
if (value && typeof value === 'object' &&
|
||||
typeof value.toJSON === 'function') {
|
||||
value = value.toJSON(key);
|
||||
}
|
||||
|
||||
// If we were called with a replacer function, then call the replacer to
|
||||
// obtain a replacement value.
|
||||
|
||||
if (typeof rep === 'function') {
|
||||
value = rep.call(holder, key, value);
|
||||
}
|
||||
|
||||
// What happens next depends on the value's type.
|
||||
|
||||
switch (typeof value) {
|
||||
case 'string':
|
||||
return quote(value);
|
||||
|
||||
case 'number':
|
||||
|
||||
// JSON numbers must be finite. Encode non-finite numbers as null.
|
||||
|
||||
return isFinite(value) ? String(value) : 'null';
|
||||
|
||||
case 'boolean':
|
||||
case 'null':
|
||||
|
||||
// If the value is a boolean or null, convert it to a string. Note:
|
||||
// typeof null does not produce 'null'. The case is included here in
|
||||
// the remote chance that this gets fixed someday.
|
||||
|
||||
return String(value);
|
||||
|
||||
// If the type is 'object', we might be dealing with an object or an array or
|
||||
// null.
|
||||
|
||||
case 'object':
|
||||
|
||||
// Due to a specification blunder in ECMAScript, typeof null is 'object',
|
||||
// so watch out for that case.
|
||||
|
||||
if (!value) {
|
||||
return 'null';
|
||||
}
|
||||
|
||||
// Make an array to hold the partial results of stringifying this object value.
|
||||
|
||||
gap += indent;
|
||||
partial = [];
|
||||
|
||||
// Is the value an array?
|
||||
|
||||
if (Object.prototype.toString.apply(value) === '[object Array]') {
|
||||
|
||||
// The value is an array. Stringify every element. Use null as a placeholder
|
||||
// for non-JSON values.
|
||||
|
||||
length = value.length;
|
||||
for (i = 0; i < length; i += 1) {
|
||||
partial[i] = str(i, value) || 'null';
|
||||
}
|
||||
|
||||
// Join all of the elements together, separated with commas, and wrap them in
|
||||
// brackets.
|
||||
|
||||
v = partial.length === 0
|
||||
? '[]'
|
||||
: gap
|
||||
? '[\n' + gap + partial.join(',\n' + gap) + '\n' + mind + ']'
|
||||
: '[' + partial.join(',') + ']';
|
||||
gap = mind;
|
||||
return v;
|
||||
}
|
||||
|
||||
// If the replacer is an array, use it to select the members to be stringified.
|
||||
|
||||
if (rep && typeof rep === 'object') {
|
||||
length = rep.length;
|
||||
for (i = 0; i < length; i += 1) {
|
||||
if (typeof rep[i] === 'string') {
|
||||
k = rep[i];
|
||||
v = str(k, value);
|
||||
if (v) {
|
||||
partial.push(quote(k) + (gap ? ': ' : ':') + v);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
// Otherwise, iterate through all of the keys in the object.
|
||||
|
||||
for (k in value) {
|
||||
if (Object.prototype.hasOwnProperty.call(value, k)) {
|
||||
v = str(k, value);
|
||||
if (v) {
|
||||
partial.push(quote(k) + (gap ? ': ' : ':') + v);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Join all of the member texts together, separated with commas,
|
||||
// and wrap them in braces.
|
||||
|
||||
v = partial.length === 0
|
||||
? '{}'
|
||||
: gap
|
||||
? '{\n' + gap + partial.join(',\n' + gap) + '\n' + mind + '}'
|
||||
: '{' + partial.join(',') + '}';
|
||||
gap = mind;
|
||||
return v;
|
||||
}
|
||||
}
|
||||
|
||||
// If the JSON object does not yet have a stringify method, give it one.
|
||||
|
||||
if (typeof JSON.stringify !== 'function') {
|
||||
JSON.stringify = function (value, replacer, space) {
|
||||
|
||||
// The stringify method takes a value and an optional replacer, and an optional
|
||||
// space parameter, and returns a JSON text. The replacer can be a function
|
||||
// that can replace values, or an array of strings that will select the keys.
|
||||
// A default replacer method can be provided. Use of the space parameter can
|
||||
// produce text that is more easily readable.
|
||||
|
||||
var i;
|
||||
gap = '';
|
||||
indent = '';
|
||||
|
||||
// If the space parameter is a number, make an indent string containing that
|
||||
// many spaces.
|
||||
|
||||
if (typeof space === 'number') {
|
||||
for (i = 0; i < space; i += 1) {
|
||||
indent += ' ';
|
||||
}
|
||||
|
||||
// If the space parameter is a string, it will be used as the indent string.
|
||||
|
||||
} else if (typeof space === 'string') {
|
||||
indent = space;
|
||||
}
|
||||
|
||||
// If there is a replacer, it must be a function or an array.
|
||||
// Otherwise, throw an error.
|
||||
|
||||
rep = replacer;
|
||||
if (replacer && typeof replacer !== 'function' &&
|
||||
(typeof replacer !== 'object' ||
|
||||
typeof replacer.length !== 'number')) {
|
||||
throw new Error('JSON.stringify');
|
||||
}
|
||||
|
||||
// Make a fake root object containing our value under the key of ''.
|
||||
// Return the result of stringifying the value.
|
||||
|
||||
return str('', {'': value});
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
// If the JSON object does not yet have a parse method, give it one.
|
||||
|
||||
if (typeof JSON.parse !== 'function') {
|
||||
JSON.parse = function (text, reviver) {
|
||||
|
||||
// The parse method takes a text and an optional reviver function, and returns
|
||||
// a JavaScript value if the text is a valid JSON text.
|
||||
|
||||
var j;
|
||||
|
||||
function walk(holder, key) {
|
||||
|
||||
// The walk method is used to recursively walk the resulting structure so
|
||||
// that modifications can be made.
|
||||
|
||||
var k, v, value = holder[key];
|
||||
if (value && typeof value === 'object') {
|
||||
for (k in value) {
|
||||
if (Object.prototype.hasOwnProperty.call(value, k)) {
|
||||
v = walk(value, k);
|
||||
if (v !== undefined) {
|
||||
value[k] = v;
|
||||
} else {
|
||||
delete value[k];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return reviver.call(holder, key, value);
|
||||
}
|
||||
|
||||
|
||||
// Parsing happens in four stages. In the first stage, we replace certain
|
||||
// Unicode characters with escape sequences. JavaScript handles many characters
|
||||
// incorrectly, either silently deleting them, or treating them as line endings.
|
||||
|
||||
text = String(text);
|
||||
cx.lastIndex = 0;
|
||||
if (cx.test(text)) {
|
||||
text = text.replace(cx, function (a) {
|
||||
return '\\u' +
|
||||
('0000' + a.charCodeAt(0).toString(16)).slice(-4);
|
||||
});
|
||||
}
|
||||
|
||||
// In the second stage, we run the text against regular expressions that look
|
||||
// for non-JSON patterns. We are especially concerned with '()' and 'new'
|
||||
// because they can cause invocation, and '=' because it can cause mutation.
|
||||
// But just to be safe, we want to reject all unexpected forms.
|
||||
|
||||
// We split the second stage into 4 regexp operations in order to work around
|
||||
// crippling inefficiencies in IE's and Safari's regexp engines. First we
|
||||
// replace the JSON backslash pairs with '@' (a non-JSON character). Second, we
|
||||
// replace all simple value tokens with ']' characters. Third, we delete all
|
||||
// open brackets that follow a colon or comma or that begin the text. Finally,
|
||||
// we look to see that the remaining characters are only whitespace or ']' or
|
||||
// ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval.
|
||||
|
||||
if (/^[\],:{}\s]*$/
|
||||
.test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@')
|
||||
.replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']')
|
||||
.replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) {
|
||||
|
||||
// In the third stage we use the eval function to compile the text into a
|
||||
// JavaScript structure. The '{' operator is subject to a syntactic ambiguity
|
||||
// in JavaScript: it can begin a block or an object literal. We wrap the text
|
||||
// in parens to eliminate the ambiguity.
|
||||
|
||||
j = eval('(' + text + ')');
|
||||
|
||||
// In the optional fourth stage, we recursively walk the new structure, passing
|
||||
// each name/value pair to a reviver function for possible transformation.
|
||||
|
||||
return typeof reviver === 'function'
|
||||
? walk({'': j}, '')
|
||||
: j;
|
||||
}
|
||||
|
||||
// If the text is not JSON parseable, then a SyntaxError is thrown.
|
||||
|
||||
throw new SyntaxError('JSON.parse');
|
||||
};
|
||||
}
|
||||
}());
|
||||
|
||||
module.exports = JSON
|
||||
});
|
||||
require.register("segmentio-json/index.js", function(exports, require, module){
|
||||
|
||||
module.exports = 'undefined' == typeof JSON
|
||||
? require('json-fallback')
|
||||
: JSON;
|
||||
|
||||
});
|
||||
require.register("segmentio-load-date/index.js", function(exports, require, module){
|
||||
|
||||
|
||||
/*
|
||||
* Load date.
|
||||
*
|
||||
* For reference: http://www.html5rocks.com/en/tutorials/webperformance/basics/
|
||||
*/
|
||||
|
||||
var time = new Date()
|
||||
, perf = window.performance;
|
||||
|
||||
if (perf && perf.timing && perf.timing.responseEnd) {
|
||||
time = new Date(perf.timing.responseEnd);
|
||||
}
|
||||
|
||||
module.exports = time;
|
||||
});
|
||||
require.register("segmentio-load-script/index.js", function(exports, require, module){
|
||||
var type = require('type');
|
||||
|
||||
|
||||
module.exports = function loadScript (options, callback) {
|
||||
if (!options) throw new Error('Cant load nothing...');
|
||||
|
||||
// Allow for the simplest case, just passing a `src` string.
|
||||
if (type(options) === 'string') options = { src : options };
|
||||
|
||||
var https = document.location.protocol === 'https:';
|
||||
|
||||
// If you use protocol relative URLs, third-party scripts like Google
|
||||
// Analytics break when testing with `file:` so this fixes that.
|
||||
if (options.src && options.src.indexOf('//') === 0) {
|
||||
options.src = https ? 'https:' + options.src : 'http:' + options.src;
|
||||
}
|
||||
|
||||
// Allow them to pass in different URLs depending on the protocol.
|
||||
if (https && options.https) options.src = options.https;
|
||||
else if (!https && options.http) options.src = options.http;
|
||||
|
||||
// Make the `<script>` element and insert it before the first script on the
|
||||
// page, which is guaranteed to exist since this Javascript is running.
|
||||
var script = document.createElement('script');
|
||||
script.type = 'text/javascript';
|
||||
script.async = true;
|
||||
script.src = options.src;
|
||||
|
||||
var firstScript = document.getElementsByTagName('script')[0];
|
||||
firstScript.parentNode.insertBefore(script, firstScript);
|
||||
|
||||
// If we have a callback, attach event handlers, even in IE. Based off of
|
||||
// the Third-Party Javascript script loading example:
|
||||
// https://github.com/thirdpartyjs/thirdpartyjs-code/blob/master/examples/templates/02/loading-files/index.html
|
||||
if (callback && type(callback) === 'function') {
|
||||
if (script.addEventListener) {
|
||||
script.addEventListener('load', callback, false);
|
||||
} else if (script.attachEvent) {
|
||||
script.attachEvent('onreadystatechange', function () {
|
||||
if (/complete|loaded/.test(script.readyState)) callback();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Return the script element in case they want to do anything special, like
|
||||
// give it an ID or attributes.
|
||||
return script;
|
||||
};
|
||||
});
|
||||
require.register("segmentio-new-date/index.js", function(exports, require, module){
|
||||
var type = require('type');
|
||||
|
||||
|
||||
/**
|
||||
* Returns a new Javascript Date object, allowing a variety of extra input types
|
||||
* over the native one.
|
||||
*
|
||||
* @param {Date|String|Number} input
|
||||
*/
|
||||
|
||||
module.exports = function newDate (input) {
|
||||
|
||||
// Convert input from seconds to milliseconds.
|
||||
input = toMilliseconds(input);
|
||||
|
||||
// By default, delegate to Date, which will return `Invalid Date`s if wrong.
|
||||
var date = new Date(input);
|
||||
|
||||
// If we have a string that the Date constructor couldn't parse, convert it.
|
||||
if (isNaN(date.getTime()) && 'string' === type(input)) {
|
||||
var milliseconds = toMilliseconds(parseInt(input, 10));
|
||||
date = new Date(milliseconds);
|
||||
}
|
||||
|
||||
return date;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* If the number passed in is seconds from the epoch, turn it into milliseconds.
|
||||
* Milliseconds would be greater than 31557600000 (December 31, 1970).
|
||||
*
|
||||
* @param seconds
|
||||
*/
|
||||
|
||||
function toMilliseconds (seconds) {
|
||||
if ('number' === type(seconds) && seconds < 31557600000) return seconds * 1000;
|
||||
return seconds;
|
||||
}
|
||||
});
|
||||
require.register("segmentio-on-body/index.js", function(exports, require, module){
|
||||
var each = require('each');
|
||||
|
||||
|
||||
/**
|
||||
* Cache whether `<body>` exists.
|
||||
*/
|
||||
|
||||
var body = false;
|
||||
|
||||
|
||||
/**
|
||||
* Callbacks to call when the body exists.
|
||||
*/
|
||||
|
||||
var callbacks = [];
|
||||
|
||||
|
||||
/**
|
||||
* Export a way to add handlers to be invoked once the body exists.
|
||||
*
|
||||
* @param {Function} callback A function to call when the body exists.
|
||||
*/
|
||||
|
||||
module.exports = function onBody (callback) {
|
||||
if (body) {
|
||||
call(callback);
|
||||
} else {
|
||||
callbacks.push(callback);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Set an interval to check for `document.body`.
|
||||
*/
|
||||
|
||||
var interval = setInterval(function () {
|
||||
if (!document.body) return;
|
||||
body = true;
|
||||
each(callbacks, call);
|
||||
clearInterval(interval);
|
||||
}, 5);
|
||||
|
||||
|
||||
/**
|
||||
* Call a callback, passing it the body.
|
||||
*
|
||||
* @param {Function} callback The callback to call.
|
||||
*/
|
||||
|
||||
function call (callback) {
|
||||
callback(document.body);
|
||||
}
|
||||
});
|
||||
require.register("segmentio-store.js/store.js", function(exports, require, module){
|
||||
var json = require('json')
|
||||
, store = {}
|
||||
, win = window
|
||||
, doc = win.document
|
||||
, localStorageName = 'localStorage'
|
||||
, namespace = '__storejs__'
|
||||
, storage;
|
||||
|
||||
store.disabled = false
|
||||
store.set = function(key, value) {}
|
||||
store.get = function(key) {}
|
||||
store.remove = function(key) {}
|
||||
store.clear = function() {}
|
||||
store.transact = function(key, defaultVal, transactionFn) {
|
||||
var val = store.get(key)
|
||||
if (transactionFn == null) {
|
||||
transactionFn = defaultVal
|
||||
defaultVal = null
|
||||
}
|
||||
if (typeof val == 'undefined') { val = defaultVal || {} }
|
||||
transactionFn(val)
|
||||
store.set(key, val)
|
||||
}
|
||||
store.getAll = function() {}
|
||||
|
||||
store.serialize = function(value) {
|
||||
return json.stringify(value)
|
||||
}
|
||||
store.deserialize = function(value) {
|
||||
if (typeof value != 'string') { return undefined }
|
||||
try { return json.parse(value) }
|
||||
catch(e) { return value || undefined }
|
||||
}
|
||||
|
||||
// Functions to encapsulate questionable FireFox 3.6.13 behavior
|
||||
// when about.config::dom.storage.enabled === false
|
||||
// See https://github.com/marcuswestin/store.js/issues#issue/13
|
||||
function isLocalStorageNameSupported() {
|
||||
try { return (localStorageName in win && win[localStorageName]) }
|
||||
catch(err) { return false }
|
||||
}
|
||||
|
||||
if (isLocalStorageNameSupported()) {
|
||||
storage = win[localStorageName]
|
||||
store.set = function(key, val) {
|
||||
if (val === undefined) { return store.remove(key) }
|
||||
storage.setItem(key, store.serialize(val))
|
||||
return val
|
||||
}
|
||||
store.get = function(key) { return store.deserialize(storage.getItem(key)) }
|
||||
store.remove = function(key) { storage.removeItem(key) }
|
||||
store.clear = function() { storage.clear() }
|
||||
store.getAll = function() {
|
||||
var ret = {}
|
||||
for (var i=0; i<storage.length; ++i) {
|
||||
var key = storage.key(i)
|
||||
ret[key] = store.get(key)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
} else if (doc.documentElement.addBehavior) {
|
||||
var storageOwner,
|
||||
storageContainer
|
||||
// Since #userData storage applies only to specific paths, we need to
|
||||
// somehow link our data to a specific path. We choose /favicon.ico
|
||||
// as a pretty safe option, since all browsers already make a request to
|
||||
// this URL anyway and being a 404 will not hurt us here. We wrap an
|
||||
// iframe pointing to the favicon in an ActiveXObject(htmlfile) object
|
||||
// (see: http://msdn.microsoft.com/en-us/library/aa752574(v=VS.85).aspx)
|
||||
// since the iframe access rules appear to allow direct access and
|
||||
// manipulation of the document element, even for a 404 page. This
|
||||
// document can be used instead of the current document (which would
|
||||
// have been limited to the current path) to perform #userData storage.
|
||||
try {
|
||||
storageContainer = new ActiveXObject('htmlfile')
|
||||
storageContainer.open()
|
||||
storageContainer.write('<s' + 'cript>document.w=window</s' + 'cript><iframe src="/favicon.ico"></iframe>')
|
||||
storageContainer.close()
|
||||
storageOwner = storageContainer.w.frames[0].document
|
||||
storage = storageOwner.createElement('div')
|
||||
} catch(e) {
|
||||
// somehow ActiveXObject instantiation failed (perhaps some special
|
||||
// security settings or otherwse), fall back to per-path storage
|
||||
storage = doc.createElement('div')
|
||||
storageOwner = doc.body
|
||||
}
|
||||
function withIEStorage(storeFunction) {
|
||||
return function() {
|
||||
var args = Array.prototype.slice.call(arguments, 0)
|
||||
args.unshift(storage)
|
||||
// See http://msdn.microsoft.com/en-us/library/ms531081(v=VS.85).aspx
|
||||
// and http://msdn.microsoft.com/en-us/library/ms531424(v=VS.85).aspx
|
||||
storageOwner.appendChild(storage)
|
||||
storage.addBehavior('#default#userData')
|
||||
storage.load(localStorageName)
|
||||
var result = storeFunction.apply(store, args)
|
||||
storageOwner.removeChild(storage)
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
// In IE7, keys may not contain special chars. See all of https://github.com/marcuswestin/store.js/issues/40
|
||||
var forbiddenCharsRegex = new RegExp("[!\"#$%&'()*+,/\\\\:;<=>?@[\\]^`{|}~]", "g")
|
||||
function ieKeyFix(key) {
|
||||
return key.replace(forbiddenCharsRegex, '___')
|
||||
}
|
||||
store.set = withIEStorage(function(storage, key, val) {
|
||||
key = ieKeyFix(key)
|
||||
if (val === undefined) { return store.remove(key) }
|
||||
storage.setAttribute(key, store.serialize(val))
|
||||
storage.save(localStorageName)
|
||||
return val
|
||||
})
|
||||
store.get = withIEStorage(function(storage, key) {
|
||||
key = ieKeyFix(key)
|
||||
return store.deserialize(storage.getAttribute(key))
|
||||
})
|
||||
store.remove = withIEStorage(function(storage, key) {
|
||||
key = ieKeyFix(key)
|
||||
storage.removeAttribute(key)
|
||||
storage.save(localStorageName)
|
||||
})
|
||||
store.clear = withIEStorage(function(storage) {
|
||||
var attributes = storage.XMLDocument.documentElement.attributes
|
||||
storage.load(localStorageName)
|
||||
for (var i=0, attr; attr=attributes[i]; i++) {
|
||||
storage.removeAttribute(attr.name)
|
||||
}
|
||||
storage.save(localStorageName)
|
||||
})
|
||||
store.getAll = withIEStorage(function(storage) {
|
||||
var attributes = storage.XMLDocument.documentElement.attributes
|
||||
var ret = {}
|
||||
for (var i=0, attr; attr=attributes[i]; ++i) {
|
||||
var key = ieKeyFix(attr.name)
|
||||
ret[attr.name] = store.deserialize(storage.getAttribute(key))
|
||||
}
|
||||
return ret
|
||||
})
|
||||
}
|
||||
|
||||
try {
|
||||
store.set(namespace, namespace)
|
||||
if (store.get(namespace) != namespace) { store.disabled = true }
|
||||
store.remove(namespace)
|
||||
} catch(e) {
|
||||
store.disabled = true
|
||||
}
|
||||
store.enabled = !store.disabled
|
||||
|
||||
module.exports = store;
|
||||
});
|
||||
require.register("segmentio-top-domain/index.js", function(exports, require, module){
|
||||
|
||||
var url = require('url');
|
||||
|
||||
// Official Grammar: http://tools.ietf.org/html/rfc883#page-56
|
||||
// Look for tlds with up to 2-6 characters.
|
||||
|
||||
module.exports = function (urlStr) {
|
||||
|
||||
var host = url.parse(urlStr).hostname
|
||||
, topLevel = host.match(/[a-z0-9][a-z0-9\-]*[a-z0-9]\.[a-z\.]{2,6}$/i);
|
||||
|
||||
return topLevel ? topLevel[0] : host;
|
||||
};
|
||||
});
|
||||
require.register("timoxley-next-tick/index.js", function(exports, require, module){
|
||||
"use strict"
|
||||
|
||||
if (typeof setImmediate == 'function') {
|
||||
module.exports = function(f){ setImmediate(f) }
|
||||
}
|
||||
// legacy node.js
|
||||
else if (typeof process != 'undefined' && typeof process.nextTick == 'function') {
|
||||
module.exports = process.nextTick
|
||||
}
|
||||
// fallback for other environments / postMessage behaves badly on IE8
|
||||
else if (typeof window == 'undefined' || window.ActiveXObject || !window.postMessage) {
|
||||
module.exports = function(f){ setTimeout(f) };
|
||||
} else {
|
||||
var q = [];
|
||||
|
||||
window.addEventListener('message', function(){
|
||||
var i = 0;
|
||||
while (i < q.length) {
|
||||
try { q[i++](); }
|
||||
catch (e) {
|
||||
q = q.slice(i);
|
||||
window.postMessage('tic!', '*');
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
q.length = 0;
|
||||
}, true);
|
||||
|
||||
module.exports = function(fn){
|
||||
if (!q.length) window.postMessage('tic!', '*');
|
||||
q.push(fn);
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
require.register("yields-prevent/index.js", function(exports, require, module){
|
||||
|
||||
/**
|
||||
* prevent default on the given `e`.
|
||||
*
|
||||
* examples:
|
||||
*
|
||||
* anchor.onclick = prevent;
|
||||
* anchor.onclick = function(e){
|
||||
* if (something) return prevent(e);
|
||||
* };
|
||||
*
|
||||
* @param {Event} e
|
||||
*/
|
||||
|
||||
module.exports = function(e){
|
||||
e = e || window.event
|
||||
return e.preventDefault
|
||||
? e.preventDefault()
|
||||
: e.returnValue = false;
|
||||
};
|
||||
|
||||
});
|
||||
require.register("analytics/src/index.js", function(exports, require, module){
|
||||
// Analytics.js
|
||||
//
|
||||
// (c) 2013 Segment.io Inc.
|
||||
// Analytics.js may be freely distributed under the MIT license.
|
||||
|
||||
var Analytics = require('./analytics')
|
||||
, providers = require('./providers');
|
||||
|
||||
|
||||
module.exports = new Analytics(providers);
|
||||
});
|
||||
require.register("analytics/src/analytics.js", function(exports, require, module){
|
||||
var after = require('after')
|
||||
, bind = require('event').bind
|
||||
, clone = require('clone')
|
||||
, cookie = require('./cookie')
|
||||
, each = require('each')
|
||||
, extend = require('extend')
|
||||
, isEmail = require('is-email')
|
||||
, isMeta = require('is-meta')
|
||||
, localStore = require('./localStore')
|
||||
, newDate = require('new-date')
|
||||
, size = require('object').length
|
||||
, preventDefault = require('prevent')
|
||||
, Provider = require('./provider')
|
||||
, providers = require('./providers')
|
||||
, querystring = require('querystring')
|
||||
, type = require('type')
|
||||
, url = require('url')
|
||||
, user = require('./user')
|
||||
, utils = require('./utils');
|
||||
|
||||
|
||||
module.exports = Analytics;
|
||||
|
||||
|
||||
/**
|
||||
* Analytics.
|
||||
*
|
||||
* @param {Object} Providers - Provider classes that the user can initialize.
|
||||
*/
|
||||
|
||||
function Analytics (Providers) {
|
||||
var self = this;
|
||||
|
||||
this.VERSION = '0.11.9';
|
||||
|
||||
each(Providers, function (Provider) {
|
||||
self.addProvider(Provider);
|
||||
});
|
||||
|
||||
// Wrap `onload` with our own that will cache the loaded state of the page.
|
||||
var oldonload = window.onload;
|
||||
window.onload = function () {
|
||||
self.loaded = true;
|
||||
if ('function' === type(oldonload)) oldonload();
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Extend the Analytics prototype.
|
||||
*/
|
||||
|
||||
extend(Analytics.prototype, {
|
||||
|
||||
// Whether `onload` has fired.
|
||||
loaded : false,
|
||||
|
||||
// Whether `analytics` has been initialized.
|
||||
initialized : false,
|
||||
|
||||
// Whether all of our analytics providers are ready to accept calls. Give it a
|
||||
// real jank name since we already use `analytics.ready` for the method.
|
||||
readied : false,
|
||||
|
||||
// A queue for ready callbacks to run when our `readied` state becomes `true`.
|
||||
callbacks : [],
|
||||
|
||||
// Milliseconds to wait for requests to clear before leaving the current page.
|
||||
timeout : 300,
|
||||
|
||||
// A reference to the current user object.
|
||||
user : user,
|
||||
|
||||
// The default Provider.
|
||||
Provider : Provider,
|
||||
|
||||
// Providers that can be initialized. Add using `this.addProvider`.
|
||||
_providers : {},
|
||||
|
||||
// The currently initialized providers.
|
||||
providers : [],
|
||||
|
||||
|
||||
/**
|
||||
* Add a provider to `_providers` to be initialized later.
|
||||
*
|
||||
* @param {String} name - The name of the provider.
|
||||
* @param {Function} Provider - The provider's class.
|
||||
*/
|
||||
|
||||
addProvider : function (Provider) {
|
||||
this._providers[Provider.prototype.name] = Provider;
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Initialize
|
||||
*
|
||||
* Call `initialize` to setup analytics.js before identifying or
|
||||
* tracking any users or events. For example:
|
||||
*
|
||||
* analytics.initialize({
|
||||
* 'Google Analytics' : 'UA-XXXXXXX-X',
|
||||
* 'Segment.io' : 'XXXXXXXXXXX',
|
||||
* 'KISSmetrics' : 'XXXXXXXXXXX'
|
||||
* });
|
||||
*
|
||||
* @param {Object} providers - a dictionary of the providers you want to
|
||||
* enable. The keys are the names of the providers and their values are either
|
||||
* an api key, or dictionary of extra settings (including the api key).
|
||||
*
|
||||
* @param {Object} options (optional) - extra settings to initialize with.
|
||||
*/
|
||||
|
||||
initialize : function (providers, options) {
|
||||
options || (options = {});
|
||||
|
||||
var self = this;
|
||||
|
||||
// Reset our state.
|
||||
this.providers = [];
|
||||
this.initialized = false;
|
||||
this.readied = false;
|
||||
|
||||
// Set the storage options
|
||||
cookie.options(options.cookie);
|
||||
localStore.options(options.localStorage);
|
||||
|
||||
// Set the options for loading and saving the user
|
||||
user.options(options.user);
|
||||
user.load();
|
||||
|
||||
// Create a ready method that will call all of our ready callbacks after all
|
||||
// of our providers have been initialized and loaded. We'll pass the
|
||||
// function into each provider's initialize method, so they can callback
|
||||
// after they've loaded successfully.
|
||||
var ready = after(size(providers), function () {
|
||||
self.readied = true;
|
||||
var callback;
|
||||
while(callback = self.callbacks.shift()) {
|
||||
callback();
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize a new instance of each provider with their `options`, and
|
||||
// copy the provider into `this.providers`.
|
||||
each(providers, function (key, options) {
|
||||
var Provider = self._providers[key];
|
||||
if (!Provider) return;
|
||||
self.providers.push(new Provider(options, ready, self));
|
||||
});
|
||||
|
||||
// Identify and track any `ajs_uid` and `ajs_event` parameters in the URL.
|
||||
var query = url.parse(window.location.href).query;
|
||||
var queries = querystring.parse(query);
|
||||
if (queries.ajs_uid) this.identify(queries.ajs_uid);
|
||||
if (queries.ajs_event) this.track(queries.ajs_event);
|
||||
|
||||
// Update the initialized state that other methods rely on.
|
||||
this.initialized = true;
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Ready
|
||||
*
|
||||
* Add a callback that will get called when all of the analytics services you
|
||||
* initialize are ready to be called. It's like jQuery's `ready` except for
|
||||
* analytics instead of the DOM.
|
||||
*
|
||||
* If we're already ready, it will callback immediately.
|
||||
*
|
||||
* @param {Function} callback - The callback to attach.
|
||||
*/
|
||||
|
||||
ready : function (callback) {
|
||||
if (type(callback) !== 'function') return;
|
||||
if (this.readied) return callback();
|
||||
this.callbacks.push(callback);
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Identify
|
||||
*
|
||||
* Identifying a user ties all of their actions to an ID you recognize
|
||||
* and records properties about a user. For example:
|
||||
*
|
||||
* analytics.identify('4d3ed089fb60ab534684b7e0', {
|
||||
* name : 'Achilles',
|
||||
* email : 'achilles@segment.io',
|
||||
* age : 23
|
||||
* });
|
||||
*
|
||||
* @param {String} userId (optional) - The ID you recognize the user by.
|
||||
* Ideally this isn't an email, because that might change in the future.
|
||||
*
|
||||
* @param {Object} traits (optional) - A dictionary of traits you know about
|
||||
* the user. Things like `name`, `age`, etc.
|
||||
*
|
||||
* @param {Object} options (optional) - Settings for the identify call.
|
||||
*
|
||||
* @param {Function} callback (optional) - A function to call after a small
|
||||
* timeout, giving the identify call time to make requests.
|
||||
*/
|
||||
|
||||
identify : function (userId, traits, options, callback) {
|
||||
if (!this.initialized) return;
|
||||
|
||||
// Allow for optional arguments.
|
||||
if (type(options) === 'function') {
|
||||
callback = options;
|
||||
options = undefined;
|
||||
}
|
||||
if (type(traits) === 'function') {
|
||||
callback = traits;
|
||||
traits = undefined;
|
||||
}
|
||||
if (type(userId) === 'object') {
|
||||
if (traits && type(traits) === 'function') callback = traits;
|
||||
traits = userId;
|
||||
userId = undefined;
|
||||
}
|
||||
|
||||
// Use our cookied ID if they didn't provide one.
|
||||
if (userId === undefined || user === null) userId = user.id();
|
||||
|
||||
// Update the cookie with the new userId and traits.
|
||||
var alias = user.update(userId, traits);
|
||||
|
||||
// Clone `traits` before we manipulate it, so we don't do anything uncouth
|
||||
// and take the user.traits() so anonymous users carry over traits.
|
||||
traits = cleanTraits(userId, clone(user.traits()));
|
||||
|
||||
// Call `identify` on all of our enabled providers that support it.
|
||||
each(this.providers, function (provider) {
|
||||
if (provider.identify && isEnabled(provider, options)) {
|
||||
var args = [userId, clone(traits), clone(options)];
|
||||
if (provider.ready) {
|
||||
provider.identify.apply(provider, args);
|
||||
} else {
|
||||
provider.enqueue('identify', args);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// If we should alias, go ahead and do it.
|
||||
// if (alias) this.alias(userId);
|
||||
|
||||
if (callback && type(callback) === 'function') {
|
||||
setTimeout(callback, this.timeout);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Group
|
||||
*
|
||||
* Groups multiple users together under one "account" or "team" or "company".
|
||||
* Acts on the currently identified user, so you need to call identify before
|
||||
* calling group. For example:
|
||||
*
|
||||
* analytics.identify('4d3ed089fb60ab534684b7e0', {
|
||||
* name : 'Achilles',
|
||||
* email : 'achilles@segment.io',
|
||||
* age : 23
|
||||
* });
|
||||
*
|
||||
* analytics.group('5we93je3889fb60a937dk033', {
|
||||
* name : 'Acme Co.',
|
||||
* numberOfEmployees : 42,
|
||||
* location : 'San Francisco'
|
||||
* });
|
||||
*
|
||||
* @param {String} groupId - The ID you recognize the group by.
|
||||
*
|
||||
* @param {Object} properties (optional) - A dictionary of properties you know
|
||||
* about the group. Things like `numberOfEmployees`, `location`, etc.
|
||||
*
|
||||
* @param {Object} options (optional) - Settings for the group call.
|
||||
*
|
||||
* @param {Function} callback (optional) - A function to call after a small
|
||||
* timeout, giving the group call time to make requests.
|
||||
*/
|
||||
|
||||
group : function (groupId, properties, options, callback) {
|
||||
if (!this.initialized) return;
|
||||
|
||||
// Allow for optional arguments.
|
||||
if (type(options) === 'function') {
|
||||
callback = options;
|
||||
options = undefined;
|
||||
}
|
||||
if (type(properties) === 'function') {
|
||||
callback = properties;
|
||||
properties = undefined;
|
||||
}
|
||||
|
||||
// Clone `properties` before we manipulate it, so we don't do anything bad,
|
||||
// and back it by an empty object so that providers can assume it exists.
|
||||
properties = clone(properties) || {};
|
||||
|
||||
// Convert dates from more types of input into Date objects.
|
||||
if (properties.created) properties.created = newDate(properties.created);
|
||||
|
||||
// Call `group` on all of our enabled providers that support it.
|
||||
each(this.providers, function (provider) {
|
||||
if (provider.group && isEnabled(provider, options)) {
|
||||
var args = [groupId, clone(properties), clone(options)];
|
||||
if (provider.ready) {
|
||||
provider.group.apply(provider, args);
|
||||
} else {
|
||||
provider.enqueue('group', args);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// If we have a callback, call it after a small timeout.
|
||||
if (callback && type(callback) === 'function') {
|
||||
setTimeout(callback, this.timeout);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Track
|
||||
*
|
||||
* Record an event (or action) that your user has triggered. For example:
|
||||
*
|
||||
* analytics.track('Added a Friend', {
|
||||
* level : 'hard',
|
||||
* volume : 11
|
||||
* });
|
||||
*
|
||||
* @param {String} event - The name of your event.
|
||||
*
|
||||
* @param {Object} properties (optional) - A dictionary of properties of the
|
||||
* event. `properties` are all camelCase (we'll automatically conver them to
|
||||
* the proper case each provider needs).
|
||||
*
|
||||
* @param {Object} options (optional) - Settings for the track call.
|
||||
*
|
||||
* @param {Function} callback - A function to call after a small
|
||||
* timeout, giving the identify time to make requests.
|
||||
*/
|
||||
|
||||
track : function (event, properties, options, callback) {
|
||||
if (!this.initialized) return;
|
||||
|
||||
// Allow for optional arguments.
|
||||
if (type(options) === 'function') {
|
||||
callback = options;
|
||||
options = undefined;
|
||||
}
|
||||
if (type(properties) === 'function') {
|
||||
callback = properties;
|
||||
properties = undefined;
|
||||
}
|
||||
|
||||
// Call `track` on all of our enabled providers that support it.
|
||||
each(this.providers, function (provider) {
|
||||
if (provider.track && isEnabled(provider, options)) {
|
||||
var args = [event, clone(properties), clone(options)];
|
||||
if (provider.ready) {
|
||||
provider.track.apply(provider, args);
|
||||
} else {
|
||||
provider.enqueue('track', args);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (callback && type(callback) === 'function') {
|
||||
setTimeout(callback, this.timeout);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Track Link
|
||||
*
|
||||
* A helper for tracking outbound links that would normally navigate away from
|
||||
* the page before the track requests were made. It works by wrapping the
|
||||
* calls in a short timeout, giving the requests time to fire.
|
||||
*
|
||||
* @param {Element|Array} links - The link element or array of link elements
|
||||
* to bind to. (Allowing arrays makes it easy to pass in jQuery objects.)
|
||||
*
|
||||
* @param {String|Function} event - Passed directly to `track`. Or in the case
|
||||
* that it's a function, it will be called with the link element as the first
|
||||
* argument.
|
||||
*
|
||||
* @param {Object|Function} properties (optional) - Passed directly to
|
||||
* `track`. Or in the case that it's a function, it will be called with the
|
||||
* link element as the first argument.
|
||||
*/
|
||||
|
||||
trackLink : function (links, event, properties) {
|
||||
if (!links) return;
|
||||
|
||||
// Turn a single link into an array so that we're always handling
|
||||
// arrays, which allows for passing jQuery objects.
|
||||
if ('element' === type(links)) links = [links];
|
||||
|
||||
var self = this
|
||||
, eventFunction = 'function' === type(event)
|
||||
, propertiesFunction = 'function' === type(properties);
|
||||
|
||||
each(links, function (el) {
|
||||
bind(el, 'click', function (e) {
|
||||
|
||||
// Allow for `event` or `properties` to be a function. And pass it the
|
||||
// link element that was clicked.
|
||||
var newEvent = eventFunction ? event(el) : event;
|
||||
var newProperties = propertiesFunction ? properties(el) : properties;
|
||||
|
||||
self.track(newEvent, newProperties);
|
||||
|
||||
// To justify us preventing the default behavior we must:
|
||||
//
|
||||
// * Have an `href` to use.
|
||||
// * Not have a `target="_blank"` attribute.
|
||||
// * Not have any special keys pressed, because they might be trying to
|
||||
// open in a new tab, or window, or download.
|
||||
//
|
||||
// This might not cover all cases, but we'd rather throw out an event
|
||||
// than miss a case that breaks the user experience.
|
||||
if (el.href && el.target !== '_blank' && !isMeta(e)) {
|
||||
|
||||
preventDefault(e);
|
||||
|
||||
// Navigate to the url after just enough of a timeout.
|
||||
setTimeout(function () {
|
||||
window.location.href = el.href;
|
||||
}, self.timeout);
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Track Form
|
||||
*
|
||||
* Similar to `trackClick`, this is a helper for tracking form submissions
|
||||
* that would normally navigate away from the page before a track request can
|
||||
* be sent. It works by preventing the default submit event, sending our
|
||||
* track requests, and then submitting the form programmatically.
|
||||
*
|
||||
* @param {Element|Array} forms - The form element or array of form elements
|
||||
* to bind to. (Allowing arrays makes it easy to pass in jQuery objects.)
|
||||
*
|
||||
* @param {String|Function} event - Passed directly to `track`. Or in the case
|
||||
* that it's a function, it will be called with the form element as the first
|
||||
* argument.
|
||||
*
|
||||
* @param {Object|Function} properties (optional) - Passed directly to
|
||||
* `track`. Or in the case that it's a function, it will be called with the
|
||||
* form element as the first argument.
|
||||
*/
|
||||
|
||||
trackForm : function (form, event, properties) {
|
||||
if (!form) return;
|
||||
|
||||
// Turn a single element into an array so that we're always handling arrays,
|
||||
// which allows for passing jQuery objects.
|
||||
if ('element' === type(form)) form = [form];
|
||||
|
||||
var self = this
|
||||
, eventFunction = 'function' === type(event)
|
||||
, propertiesFunction = 'function' === type(properties);
|
||||
|
||||
each(form, function (el) {
|
||||
var handler = function (e) {
|
||||
|
||||
// Allow for `event` or `properties` to be a function. And pass it the
|
||||
// form element that was submitted.
|
||||
var newEvent = eventFunction ? event(el) : event;
|
||||
var newProperties = propertiesFunction ? properties(el) : properties;
|
||||
|
||||
self.track(newEvent, newProperties);
|
||||
|
||||
preventDefault(e);
|
||||
|
||||
// Submit the form after a timeout, giving the event time to fire.
|
||||
setTimeout(function () {
|
||||
el.submit();
|
||||
}, self.timeout);
|
||||
};
|
||||
|
||||
// Support the form being submitted via jQuery instead of for real. This
|
||||
// doesn't happen automatically because `el.submit()` doesn't actually
|
||||
// fire submit handlers, which is what jQuery uses internally. >_<
|
||||
var dom = window.jQuery || window.Zepto;
|
||||
if (dom) {
|
||||
dom(el).submit(handler);
|
||||
} else {
|
||||
bind(el, 'submit', handler);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Pageview
|
||||
*
|
||||
* Simulate a pageview in single-page applications, where real pageviews don't
|
||||
* occur. This isn't support by all providers.
|
||||
*
|
||||
* @param {String} url (optional) - The path of the page (eg. '/login'). Most
|
||||
* providers will default to the current pages URL, so you don't need this.
|
||||
*
|
||||
* @param {Object} options (optional) - Settings for the pageview call.
|
||||
*
|
||||
*/
|
||||
|
||||
pageview : function (url,options) {
|
||||
if (!this.initialized) return;
|
||||
|
||||
// Call `pageview` on all of our enabled providers that support it.
|
||||
each(this.providers, function (provider) {
|
||||
if (provider.pageview && isEnabled(provider, options)) {
|
||||
var args = [url];
|
||||
if (provider.ready) {
|
||||
provider.pageview.apply(provider, args);
|
||||
} else {
|
||||
provider.enqueue('pageview', args);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Alias
|
||||
*
|
||||
* Merges two previously unassociate user identities. This comes in handy if
|
||||
* the same user visits from two different devices and you want to combine
|
||||
* their analytics history.
|
||||
*
|
||||
* Some providers don't support merging users.
|
||||
*
|
||||
* @param {String} newId - The new ID you want to recognize the user by.
|
||||
*
|
||||
* @param {String} originalId (optional) - The original ID that the user was
|
||||
* recognized by. This defaults to the current identified user's ID if there
|
||||
* is one. In most cases you don't need to pass in the `originalId`.
|
||||
*/
|
||||
|
||||
alias : function (newId, originalId, options) {
|
||||
if (!this.initialized) return;
|
||||
|
||||
if (type(originalId) === 'object') {
|
||||
options = originalId;
|
||||
originalId = undefined;
|
||||
}
|
||||
|
||||
// Call `alias` on all of our enabled providers that support it.
|
||||
each(this.providers, function (provider) {
|
||||
if (provider.alias && isEnabled(provider, options)) {
|
||||
var args = [newId, originalId];
|
||||
if (provider.ready) {
|
||||
provider.alias.apply(provider, args);
|
||||
} else {
|
||||
provider.enqueue('alias', args);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Log
|
||||
*
|
||||
* Log an error to analytics providers that support it, like Sentry.
|
||||
*
|
||||
* @param {Error|String} error - The error or string to log.
|
||||
* @param {Object} properties - Properties about the error.
|
||||
* @param {Object} options (optional) - Settings for the log call.
|
||||
*/
|
||||
|
||||
log : function (error, properties, options) {
|
||||
if (!this.initialized) return;
|
||||
|
||||
each(this.providers, function (provider) {
|
||||
if (provider.log && isEnabled(provider, options)) {
|
||||
var args = [error, properties, options];
|
||||
if (provider.ready) {
|
||||
provider.log.apply(provider, args);
|
||||
} else {
|
||||
provider.enqueue('log', args);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* Backwards compatibility.
|
||||
*/
|
||||
|
||||
// Alias `trackClick` and `trackSubmit`.
|
||||
Analytics.prototype.trackClick = Analytics.prototype.trackLink;
|
||||
Analytics.prototype.trackSubmit = Analytics.prototype.trackForm;
|
||||
|
||||
|
||||
/**
|
||||
* Determine whether a provider is enabled or not based on the options object.
|
||||
*
|
||||
* @param {Object} provider - the current provider.
|
||||
* @param {Object} options - the current call's options.
|
||||
*
|
||||
* @return {Boolean} - wether the provider is enabled.
|
||||
*/
|
||||
|
||||
var isEnabled = function (provider, options) {
|
||||
var enabled = true;
|
||||
if (!options || !options.providers) return enabled;
|
||||
|
||||
// Default to the 'all' or 'All' setting.
|
||||
var map = options.providers;
|
||||
if (map.all !== undefined) enabled = map.all;
|
||||
if (map.All !== undefined) enabled = map.All;
|
||||
|
||||
// Look for this provider's specific setting.
|
||||
var name = provider.name;
|
||||
if (map[name] !== undefined) enabled = map[name];
|
||||
|
||||
return enabled;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Clean up traits, default some useful things both so the user doesn't have to
|
||||
* and so we don't have to do it on a provider-basis.
|
||||
*
|
||||
* @param {Object} traits The traits object.
|
||||
* @return {Object} The new traits object.
|
||||
*/
|
||||
|
||||
var cleanTraits = function (userId, traits) {
|
||||
|
||||
// Add the `email` trait if it doesn't exist and the `userId` is an email.
|
||||
if (!traits.email && isEmail(userId)) traits.email = userId;
|
||||
|
||||
// Create the `name` trait if it doesn't exist and `firstName` and `lastName`
|
||||
// are both supplied.
|
||||
if (!traits.name && traits.firstName && traits.lastName) {
|
||||
traits.name = traits.firstName + ' ' + traits.lastName;
|
||||
}
|
||||
|
||||
// Convert dates from more types of input into Date objects.
|
||||
if (traits.created) traits.created = newDate(traits.created);
|
||||
if (traits.company && traits.company.created) {
|
||||
traits.company.created = newDate(traits.company.created);
|
||||
}
|
||||
|
||||
return traits;
|
||||
};
|
||||
|
||||
});
|
||||
require.register("analytics/src/cookie.js", function(exports, require, module){
|
||||
|
||||
var bindAll = require('bind-all')
|
||||
, cookie = require('cookie')
|
||||
, clone = require('clone')
|
||||
, defaults = require('defaults')
|
||||
, json = require('json')
|
||||
, topDomain = require('top-domain');
|
||||
|
||||
|
||||
function Cookie (options) {
|
||||
this.options(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or set the cookie options
|
||||
*
|
||||
* @param {Object} options
|
||||
* @field {Number} maxage (1 year)
|
||||
* @field {String} domain
|
||||
* @field {String} path
|
||||
* @field {Boolean} secure
|
||||
*/
|
||||
|
||||
Cookie.prototype.options = function (options) {
|
||||
if (arguments.length === 0) return this._options;
|
||||
|
||||
options || (options = {});
|
||||
|
||||
var domain = '.' + topDomain(window.location.href);
|
||||
|
||||
// localhost cookies are special: http://curl.haxx.se/rfc/cookie_spec.html
|
||||
if (domain === '.localhost') domain = '';
|
||||
|
||||
defaults(options, {
|
||||
maxage : 31536000000, // default to a year
|
||||
path : '/',
|
||||
domain : domain
|
||||
});
|
||||
|
||||
this._options = options;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Set a value in our cookie
|
||||
*
|
||||
* @param {String} key
|
||||
* @param {Object} value
|
||||
* @return {Boolean} saved
|
||||
*/
|
||||
|
||||
Cookie.prototype.set = function (key, value) {
|
||||
try {
|
||||
value = json.stringify(value);
|
||||
cookie(key, value, clone(this._options));
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Get a value from our cookie
|
||||
* @param {String} key
|
||||
* @return {Object} value
|
||||
*/
|
||||
|
||||
Cookie.prototype.get = function (key) {
|
||||
try {
|
||||
var value = cookie(key);
|
||||
value = value ? json.parse(value) : null;
|
||||
return value;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Remove a value from the cookie
|
||||
*
|
||||
* @param {String} key
|
||||
* @return {Boolean} removed
|
||||
*/
|
||||
|
||||
Cookie.prototype.remove = function (key) {
|
||||
try {
|
||||
cookie(key, null, clone(this._options));
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Export singleton cookie
|
||||
*/
|
||||
|
||||
module.exports = bindAll(new Cookie());
|
||||
|
||||
|
||||
module.exports.Cookie = Cookie;
|
||||
|
||||
});
|
||||
require.register("analytics/src/localStore.js", function(exports, require, module){
|
||||
|
||||
var bindAll = require('bind-all')
|
||||
, defaults = require('defaults')
|
||||
, store = require('store');
|
||||
|
||||
|
||||
function Store (options) {
|
||||
this.options(options);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Sets the options for the store
|
||||
*
|
||||
* @param {Object} options
|
||||
* @field {Boolean} enabled (true)
|
||||
*/
|
||||
|
||||
Store.prototype.options = function (options) {
|
||||
if (arguments.length === 0) return this._options;
|
||||
|
||||
options || (options = {});
|
||||
defaults(options, { enabled : true });
|
||||
|
||||
this.enabled = options.enabled && store.enabled;
|
||||
this._options = options;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Sets a value in local storage
|
||||
*
|
||||
* @param {String} key
|
||||
* @param {Object} value
|
||||
*/
|
||||
|
||||
Store.prototype.set = function (key, value) {
|
||||
if (!this.enabled) return false;
|
||||
return store.set(key, value);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Gets a value from local storage
|
||||
*
|
||||
* @param {String} key
|
||||
* @return {Object}
|
||||
*/
|
||||
|
||||
Store.prototype.get = function (key) {
|
||||
if (!this.enabled) return null;
|
||||
return store.get(key);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Removes a value from local storage
|
||||
*
|
||||
* @param {String} key
|
||||
*/
|
||||
|
||||
Store.prototype.remove = function (key) {
|
||||
if (!this.enabled) return false;
|
||||
return store.remove(key);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Singleton exports
|
||||
*/
|
||||
|
||||
module.exports = bindAll(new Store());
|
||||
});
|
||||
require.register("analytics/src/provider.js", function(exports, require, module){
|
||||
var each = require('each')
|
||||
, extend = require('extend')
|
||||
, type = require('type');
|
||||
|
||||
|
||||
module.exports = Provider;
|
||||
|
||||
|
||||
/**
|
||||
* Provider
|
||||
*
|
||||
* @param {Object} options - settings to initialize the Provider with. This will
|
||||
* be merged with the Provider's own defaults.
|
||||
*
|
||||
* @param {Function} ready - a ready callback, to be called when the provider is
|
||||
* ready to handle analytics calls.
|
||||
*/
|
||||
|
||||
function Provider (options, ready, analytics) {
|
||||
var self = this;
|
||||
|
||||
// Store the reference to the global `analytics` object.
|
||||
this.analytics = analytics;
|
||||
|
||||
// Make a queue of `{ method : 'identify', args : [] }` to unload once ready.
|
||||
this.queue = [];
|
||||
this.ready = false;
|
||||
|
||||
// Allow for `options` to only be a string if the provider has specified
|
||||
// a default `key`, in which case convert `options` into a dictionary. Also
|
||||
// allow for it to be `true`, like in Optimizely's case where there is no need
|
||||
// for any default key.
|
||||
if (type(options) !== 'object') {
|
||||
if (options === true) {
|
||||
options = {};
|
||||
} else if (this.key) {
|
||||
var key = options;
|
||||
options = {};
|
||||
options[this.key] = key;
|
||||
} else {
|
||||
throw new Error('Couldnt resolve options.');
|
||||
}
|
||||
}
|
||||
|
||||
// Extend the passed-in options with our defaults.
|
||||
this.options = extend({}, this.defaults, options);
|
||||
|
||||
// Wrap our ready function, so that it ready from our internal queue first
|
||||
// and then marks us as ready.
|
||||
var dequeue = function () {
|
||||
each(self.queue, function (call) {
|
||||
var method = call.method
|
||||
, args = call.args;
|
||||
self[method].apply(self, args);
|
||||
});
|
||||
self.ready = true;
|
||||
self.queue = [];
|
||||
ready();
|
||||
};
|
||||
|
||||
// Call our initialize method.
|
||||
this.initialize.call(this, this.options, dequeue);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Inheritance helper.
|
||||
*
|
||||
* Modeled after Backbone's `extend` method:
|
||||
* https://github.com/documentcloud/backbone/blob/master/backbone.js#L1464
|
||||
*/
|
||||
|
||||
Provider.extend = function (properties) {
|
||||
var parent = this;
|
||||
var child = function () { return parent.apply(this, arguments); };
|
||||
var Surrogate = function () { this.constructor = child; };
|
||||
Surrogate.prototype = parent.prototype;
|
||||
child.prototype = new Surrogate();
|
||||
extend(child.prototype, properties);
|
||||
return child;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Augment Provider's prototype.
|
||||
*/
|
||||
|
||||
extend(Provider.prototype, {
|
||||
|
||||
/**
|
||||
* Default settings for the provider.
|
||||
*/
|
||||
|
||||
options : {},
|
||||
|
||||
|
||||
/**
|
||||
* The single required API key for the provider. This lets us support a terse
|
||||
* initialization syntax:
|
||||
*
|
||||
* analytics.initialize({
|
||||
* 'Provider' : 'XXXXXXX'
|
||||
* });
|
||||
*
|
||||
* Only add this if the provider has a _single_ required key.
|
||||
*/
|
||||
|
||||
key : undefined,
|
||||
|
||||
|
||||
/**
|
||||
* Initialize our provider.
|
||||
*
|
||||
* @param {Object} options - the settings for the provider.
|
||||
* @param {Function} ready - a ready callback to call when we're ready to
|
||||
* start accept analytics method calls.
|
||||
*/
|
||||
initialize : function (options, ready) {
|
||||
ready();
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Adds an item to the our internal pre-ready queue.
|
||||
*
|
||||
* @param {String} method - the analytics method to call (eg. 'track').
|
||||
* @param {Object} args - the arguments to pass to the method.
|
||||
*/
|
||||
enqueue : function (method, args) {
|
||||
this.queue.push({
|
||||
method : method,
|
||||
args : args
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
});
|
||||
require.register("analytics/src/user.js", function(exports, require, module){
|
||||
var bindAll = require('bind-all')
|
||||
, clone = require('clone')
|
||||
, cookie = require('./cookie')
|
||||
, defaults = require('defaults')
|
||||
, extend = require('extend')
|
||||
, localStore = require('./localStore');
|
||||
|
||||
|
||||
function User (options) {
|
||||
this._id = null;
|
||||
this._traits = {};
|
||||
this.options(options);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Sets the options for the user
|
||||
*
|
||||
* @param {Object} options
|
||||
* @field {Object} cookie
|
||||
* @field {Object} localStorage
|
||||
* @field {Boolean} persist (true)
|
||||
*/
|
||||
|
||||
User.prototype.options = function (options) {
|
||||
options || (options = {});
|
||||
|
||||
defaults(options, {
|
||||
persist : true
|
||||
});
|
||||
|
||||
this.cookie(options.cookie);
|
||||
this.localStorage(options.localStorage);
|
||||
this.persist = options.persist;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Get or set cookie options
|
||||
*
|
||||
* @param {Object} options
|
||||
*/
|
||||
|
||||
User.prototype.cookie = function (options) {
|
||||
if (arguments.length === 0) return this.cookieOptions;
|
||||
|
||||
options || (options = {});
|
||||
defaults(options, {
|
||||
key : 'ajs_user_id',
|
||||
oldKey : 'ajs_user'
|
||||
});
|
||||
this.cookieOptions = options;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Get or set local storage options
|
||||
*
|
||||
* @param {Object} options
|
||||
*/
|
||||
|
||||
User.prototype.localStorage = function (options) {
|
||||
if (arguments.length === 0) return this.localStorageOptions;
|
||||
|
||||
options || (options = {});
|
||||
defaults(options, {
|
||||
key : 'ajs_user_traits'
|
||||
});
|
||||
this.localStorageOptions = options;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Get or set the user id
|
||||
*
|
||||
* @param {String} id
|
||||
*/
|
||||
|
||||
User.prototype.id = function (id) {
|
||||
if (arguments.length === 0) return this._id;
|
||||
this._id = id;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Get or set the user traits
|
||||
*
|
||||
* @param {Object} traits
|
||||
*/
|
||||
|
||||
User.prototype.traits = function (traits) {
|
||||
if (arguments.length === 0) return clone(this._traits);
|
||||
traits || (traits = {});
|
||||
|
||||
this._traits = traits;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Updates the current stored user with id and traits.
|
||||
*
|
||||
* @param {String} userId - the new user ID.
|
||||
* @param {Object} traits - any new traits.
|
||||
* @return {Boolean} whether alias should be called.
|
||||
*/
|
||||
|
||||
User.prototype.update = function (userId, traits) {
|
||||
|
||||
// Make an alias call if there was no previous userId, there is one
|
||||
// now, and we are using a cookie between page loads.
|
||||
var alias = !this.id() && userId && this.persist;
|
||||
|
||||
traits || (traits = {});
|
||||
|
||||
// If there is a current user and the new user isn't the same,
|
||||
// we want to just replace their traits. Otherwise extend.
|
||||
if (this.id() && userId && this.id() !== userId) this.traits(traits);
|
||||
else this.traits(extend(this.traits(), traits));
|
||||
|
||||
if (userId) this.id(userId);
|
||||
|
||||
this.save();
|
||||
|
||||
return alias;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Save the user to localstorage and cookie
|
||||
*
|
||||
* @return {Boolean} saved
|
||||
*/
|
||||
|
||||
User.prototype.save = function () {
|
||||
if (!this.persist) return false;
|
||||
|
||||
cookie.set(this.cookie().key, this.id());
|
||||
localStore.set(this.localStorage().key, this.traits());
|
||||
return true;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Loads a saved user, and set its information
|
||||
*
|
||||
* @return {Object} user
|
||||
*/
|
||||
|
||||
User.prototype.load = function () {
|
||||
if (this.loadOldCookie()) return this.toJSON();
|
||||
|
||||
var id = cookie.get(this.cookie().key)
|
||||
, traits = localStore.get(this.localStorage().key);
|
||||
|
||||
this.id(id);
|
||||
this.traits(traits);
|
||||
return this.toJSON();
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Clears the user, and removes the stored version
|
||||
*
|
||||
*/
|
||||
|
||||
User.prototype.clear = function () {
|
||||
cookie.remove(this.cookie().key);
|
||||
localStore.remove(this.localStorage().key);
|
||||
this.id(null);
|
||||
this.traits({});
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Load the old user from the cookie. Should be phased
|
||||
* out at some point
|
||||
*
|
||||
* @return {Boolean} loaded
|
||||
*/
|
||||
|
||||
User.prototype.loadOldCookie = function () {
|
||||
var user = cookie.get(this.cookie().oldKey);
|
||||
if (!user) return false;
|
||||
|
||||
this.id(user.id);
|
||||
this.traits(user.traits);
|
||||
cookie.remove(this.cookie().oldKey);
|
||||
return true;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Get the user info
|
||||
*
|
||||
* @return {Object}
|
||||
*/
|
||||
|
||||
User.prototype.toJSON = function () {
|
||||
return {
|
||||
id : this.id(),
|
||||
traits : this.traits()
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Export the new user as a singleton.
|
||||
*/
|
||||
|
||||
module.exports = bindAll(new User());
|
||||
|
||||
});
|
||||
require.register("analytics/src/utils.js", function(exports, require, module){
|
||||
// A helper to track events based on the 'anjs' url parameter
|
||||
exports.getUrlParameter = function (urlSearchParameter, paramKey) {
|
||||
var params = urlSearchParameter.replace('?', '').split('&');
|
||||
for (var i = 0; i < params.length; i += 1) {
|
||||
var param = params[i].split('=');
|
||||
if (param.length === 2 && param[0] === paramKey) {
|
||||
return decodeURIComponent(param[1]);
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
require.register("analytics/src/providers/adroll.js", function(exports, require, module){
|
||||
// https://www.adroll.com/dashboard
|
||||
|
||||
var Provider = require('../provider')
|
||||
, load = require('load-script');
|
||||
|
||||
|
||||
module.exports = Provider.extend({
|
||||
|
||||
name : 'AdRoll',
|
||||
|
||||
defaults : {
|
||||
// Adroll requires two options: `advId` and `pixId`.
|
||||
advId : null,
|
||||
pixId : null
|
||||
},
|
||||
|
||||
initialize : function (options, ready) {
|
||||
window.adroll_adv_id = options.advId;
|
||||
window.adroll_pix_id = options.pixId;
|
||||
window.__adroll_loaded = true;
|
||||
|
||||
load({
|
||||
http : 'http://a.adroll.com/j/roundtrip.js',
|
||||
https : 'https://s.adroll.com/j/roundtrip.js'
|
||||
}, ready);
|
||||
}
|
||||
|
||||
});
|
||||
});
|
||||
require.register("analytics/src/providers/amplitude.js", function(exports, require, module){
|
||||
// https://github.com/amplitude/Amplitude-Javascript
|
||||
|
||||
var Provider = require('../provider')
|
||||
, alias = require('alias')
|
||||
, load = require('load-script');
|
||||
|
||||
|
||||
module.exports = Provider.extend({
|
||||
|
||||
name : 'Amplitude',
|
||||
|
||||
key : 'apiKey',
|
||||
|
||||
defaults : {
|
||||
// Amplitude's required API key.
|
||||
apiKey : null,
|
||||
// Whether to track pageviews to Amplitude.
|
||||
pageview : false
|
||||
},
|
||||
|
||||
initialize : function (options, ready) {
|
||||
// Create the Amplitude global and queuer methods.
|
||||
(function(e,t){var r=e.amplitude||{};
|
||||
r._q=[];function i(e){r[e]=function(){r._q.push([e].concat(Array.prototype.slice.call(arguments,0)))}}
|
||||
var s=["init","logEvent","setUserId","setGlobalUserProperties","setVersionName"];
|
||||
for(var c=0;c<s.length;c++){i(s[c])}e.amplitude=r})(window,document);
|
||||
|
||||
// Load the Amplitude script and initialize with the API key.
|
||||
load('https://d24n15hnbwhuhn.cloudfront.net/libs/amplitude-1.0-min.js');
|
||||
window.amplitude.init(options.apiKey);
|
||||
|
||||
// Amplitude creates a queue, so it's ready immediately.
|
||||
ready();
|
||||
},
|
||||
|
||||
identify : function (userId, traits) {
|
||||
if (userId) window.amplitude.setUserId(userId);
|
||||
if (traits) window.amplitude.setGlobalUserProperties(traits);
|
||||
},
|
||||
|
||||
track : function (event, properties) {
|
||||
window.amplitude.logEvent(event, properties);
|
||||
},
|
||||
|
||||
pageview : function (url) {
|
||||
if (!this.options.pageview) return;
|
||||
|
||||
var properties = {
|
||||
url : url || document.location.href,
|
||||
name : document.title
|
||||
};
|
||||
|
||||
this.track('Loaded a Page', properties);
|
||||
}
|
||||
|
||||
});
|
||||
});
|
||||
require.register("analytics/src/providers/bitdeli.js", function(exports, require, module){
|
||||
// https://bitdeli.com/docs
|
||||
// https://bitdeli.com/docs/javascript-api.html
|
||||
|
||||
var Provider = require('../provider')
|
||||
, load = require('load-script');
|
||||
|
||||
|
||||
module.exports = Provider.extend({
|
||||
|
||||
name : 'Bitdeli',
|
||||
|
||||
defaults : {
|
||||
// BitDeli requires two options: `inputId` and `authToken`.
|
||||
inputId : null,
|
||||
authToken : null,
|
||||
// Whether or not to track an initial pageview when the page first
|
||||
// loads. You might not want this if you're using a single-page app.
|
||||
initialPageview : true
|
||||
},
|
||||
|
||||
|
||||
initialize : function (options, ready) {
|
||||
window._bdq = window._bdq || [];
|
||||
window._bdq.push(["setAccount", options.inputId, options.authToken]);
|
||||
|
||||
if (options.initialPageview) this.pageview();
|
||||
|
||||
load('//d2flrkr957qc5j.cloudfront.net/bitdeli.min.js');
|
||||
|
||||
// Bitdeli just uses a queue, so it's ready right away.
|
||||
ready();
|
||||
},
|
||||
|
||||
|
||||
// Bitdeli uses two separate methods: `identify` for storing the `userId`
|
||||
// and `set` for storing `traits`.
|
||||
identify : function (userId, traits) {
|
||||
if (userId) window._bdq.push(['identify', userId]);
|
||||
if (traits) window._bdq.push(['set', traits]);
|
||||
},
|
||||
|
||||
|
||||
track : function (event, properties) {
|
||||
window._bdq.push(['track', event, properties]);
|
||||
},
|
||||
|
||||
|
||||
// If `url` is undefined, Bitdeli uses the current page URL instead.
|
||||
pageview : function (url) {
|
||||
window._bdq.push(['trackPageview', url]);
|
||||
}
|
||||
|
||||
});
|
||||
});
|
||||
require.register("analytics/src/providers/bugherd.js", function(exports, require, module){
|
||||
// http://support.bugherd.com/home
|
||||
|
||||
var Provider = require('../provider')
|
||||
, load = require('load-script');
|
||||
|
||||
|
||||
module.exports = Provider.extend({
|
||||
|
||||
name : 'BugHerd',
|
||||
|
||||
key : 'apiKey',
|
||||
|
||||
defaults : {
|
||||
apiKey : null,
|
||||
// Optionally hide the feedback tab if you want to build your own.
|
||||
// http://support.bugherd.com/entries/21497629-Create-your-own-Send-Feedback-tab
|
||||
showFeedbackTab : true
|
||||
},
|
||||
|
||||
initialize : function (options, ready) {
|
||||
if (!options.showFeedbackTab) {
|
||||
window.BugHerdConfig = { "feedback" : { "hide" : true } };
|
||||
}
|
||||
load('//www.bugherd.com/sidebarv2.js?apikey=' + options.apiKey, ready);
|
||||
}
|
||||
|
||||
});
|
||||
});
|
||||
require.register("analytics/src/providers/chartbeat.js", function(exports, require, module){
|
||||
// http://chartbeat.com/docs/adding_the_code/
|
||||
// http://chartbeat.com/docs/configuration_variables/
|
||||
// http://chartbeat.com/docs/handling_virtual_page_changes/
|
||||
|
||||
var Provider = require('../provider')
|
||||
, load = require('load-script');
|
||||
|
||||
|
||||
module.exports = Provider.extend({
|
||||
|
||||
name : 'Chartbeat',
|
||||
|
||||
defaults : {
|
||||
// Chartbeat requires two options: `domain` and `uid`. All other
|
||||
// configuration options are passed straight in!
|
||||
domain : null,
|
||||
uid : null
|
||||
},
|
||||
|
||||
|
||||
initialize : function (options, ready) {
|
||||
// Since all the custom options just get passed through, update the
|
||||
// Chartbeat `_sf_async_config` variable with options.
|
||||
window._sf_async_config = options;
|
||||
|
||||
// Chartbeat's javascript should only load after the body
|
||||
// is available, see https://github.com/segmentio/analytics.js/issues/107
|
||||
var loadChartbeat = function () {
|
||||
// We loop until the body is available.
|
||||
if (!document.body) return setTimeout(loadChartbeat, 5);
|
||||
|
||||
// Use the stored date from when chartbeat was loaded.
|
||||
window._sf_endpt = (new Date()).getTime();
|
||||
|
||||
// Load the Chartbeat javascript.
|
||||
load({
|
||||
https : 'https://a248.e.akamai.net/chartbeat.download.akamai.com/102508/js/chartbeat.js',
|
||||
http : 'http://static.chartbeat.com/js/chartbeat.js'
|
||||
}, ready);
|
||||
};
|
||||
loadChartbeat();
|
||||
},
|
||||
|
||||
|
||||
pageview : function (url) {
|
||||
// In case the Chartbeat library hasn't loaded yet.
|
||||
if (!window.pSUPERFLY) return;
|
||||
|
||||
// Requires a path, so default to the current one.
|
||||
window.pSUPERFLY.virtualPage(url || window.location.pathname);
|
||||
}
|
||||
|
||||
});
|
||||
});
|
||||
require.register("analytics/src/providers/clicktale.js", function(exports, require, module){
|
||||
// http://wiki.clicktale.com/Article/JavaScript_API
|
||||
|
||||
var date = require('load-date')
|
||||
, Provider = require('../provider')
|
||||
, load = require('load-script')
|
||||
, onBody = require('on-body');
|
||||
|
||||
module.exports = Provider.extend({
|
||||
|
||||
name : 'ClickTale',
|
||||
|
||||
key : 'projectId',
|
||||
|
||||
defaults : {
|
||||
|
||||
// If you sign up for a free account, this is the default http (non-ssl) CDN URL
|
||||
// that you get. If you sign up for a premium account, you get a different
|
||||
// custom CDN URL, so we have to leave it as an option.
|
||||
httpCdnUrl : 'http://s.clicktale.net/WRe0.js',
|
||||
|
||||
// SSL support is only for premium accounts. Each premium account seems to have
|
||||
// a different custom secure CDN URL, so we have to leave it as an option.
|
||||
httpsCdnUrl : null,
|
||||
|
||||
// The Project ID is loaded in after the ClickTale CDN javascript has loaded.
|
||||
projectId : null,
|
||||
|
||||
// The recording ratio specifies what fraction of people to screen-record.
|
||||
// ClickTale has a special calculator in their setup flow that tells you
|
||||
// what number to set for this.
|
||||
recordingRatio : 0.01,
|
||||
|
||||
// The Partition ID determines where ClickTale stores the data according to
|
||||
// http://wiki.clicktale.com/Article/JavaScript_API
|
||||
partitionId : null
|
||||
},
|
||||
|
||||
|
||||
initialize : function (options, ready) {
|
||||
// If we're on https:// but don't have a secure library, return early.
|
||||
if (document.location.protocol === 'https:' && !options.httpsCdnUrl) return;
|
||||
|
||||
// ClickTale wants this at the "top" of the page. The analytics.js snippet
|
||||
// sets this date synchronously now, and makes it available via load-date.
|
||||
window.WRInitTime = date.getTime();
|
||||
|
||||
// Add the required ClickTale div to the body.
|
||||
onBody(function (body) {
|
||||
var div = document.createElement('div');
|
||||
div.setAttribute('id', 'ClickTaleDiv');
|
||||
div.setAttribute('style', 'display: none;');
|
||||
body.appendChild(div);
|
||||
});
|
||||
|
||||
var onloaded = function () {
|
||||
window.ClickTale(
|
||||
options.projectId,
|
||||
options.recordingRatio,
|
||||
options.partitionId
|
||||
);
|
||||
ready();
|
||||
};
|
||||
|
||||
// If no SSL library is provided and we're on SSL then we can't load
|
||||
// anything (always true for non-premium accounts).
|
||||
load({
|
||||
http : options.httpCdnUrl,
|
||||
https : options.httpsCdnUrl
|
||||
}, onloaded);
|
||||
},
|
||||
|
||||
identify : function (userId, traits) {
|
||||
// We set the userId as the ClickTale UID.
|
||||
if (window.ClickTaleSetUID) window.ClickTaleSetUID(userId);
|
||||
|
||||
// We iterate over all the traits and set them as key-value field pairs.
|
||||
if (window.ClickTaleField) {
|
||||
for (var traitKey in traits) {
|
||||
window.ClickTaleField(traitKey, traits[traitKey]);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
track : function (event, properties) {
|
||||
// ClickTaleEvent is an alias for ClickTaleTag
|
||||
if (window.ClickTaleEvent) window.ClickTaleEvent(event);
|
||||
}
|
||||
|
||||
});
|
||||
});
|
||||
require.register("analytics/src/providers/clicky.js", function(exports, require, module){
|
||||
// http://clicky.com/help/customization/manual?new-domain
|
||||
// http://clicky.com/help/customization/manual?new-domain#/help/customization#session
|
||||
|
||||
var Provider = require('../provider')
|
||||
, user = require('../user')
|
||||
, extend = require('extend')
|
||||
, load = require('load-script');
|
||||
|
||||
|
||||
module.exports = Provider.extend({
|
||||
|
||||
name : 'Clicky',
|
||||
|
||||
key : 'siteId',
|
||||
|
||||
defaults : {
|
||||
siteId : null
|
||||
},
|
||||
|
||||
initialize : function (options, ready) {
|
||||
window.clicky_site_ids = window.clicky_site_ids || [];
|
||||
window.clicky_site_ids.push(options.siteId);
|
||||
|
||||
var userId = user.id()
|
||||
, traits = user.traits()
|
||||
, session = {};
|
||||
|
||||
if (userId) session.id = userId;
|
||||
extend(session, traits);
|
||||
|
||||
window.clicky_custom = { session : session };
|
||||
|
||||
load('//static.getclicky.com/js', ready);
|
||||
},
|
||||
|
||||
track : function (event, properties) {
|
||||
window.clicky.log(window.location.href, event);
|
||||
}
|
||||
|
||||
});
|
||||
});
|
||||
require.register("analytics/src/providers/comscore.js", function(exports, require, module){
|
||||
// http://direct.comscore.com/clients/help/FAQ.aspx#faqTagging
|
||||
|
||||
var Provider = require('../provider')
|
||||
, load = require('load-script');
|
||||
|
||||
|
||||
module.exports = Provider.extend({
|
||||
|
||||
name : 'comScore',
|
||||
|
||||
key : 'c2',
|
||||
|
||||
defaults : {
|
||||
c1 : '2',
|
||||
c2 : null
|
||||
},
|
||||
|
||||
// Pass the entire options object directly into comScore.
|
||||
initialize : function (options, ready) {
|
||||
window._comscore = window._comscore || [];
|
||||
window._comscore.push(options);
|
||||
load({
|
||||
http : 'http://b.scorecardresearch.com/beacon.js',
|
||||
https : 'https://sb.scorecardresearch.com/beacon.js'
|
||||
}, ready);
|
||||
}
|
||||
|
||||
});
|
||||
});
|
||||
require.register("analytics/src/providers/crazyegg.js", function(exports, require, module){
|
||||
var Provider = require('../provider')
|
||||
, load = require('load-script');
|
||||
|
||||
|
||||
module.exports = Provider.extend({
|
||||
|
||||
name : 'CrazyEgg',
|
||||
|
||||
key : 'accountNumber',
|
||||
|
||||
defaults : {
|
||||
accountNumber : null
|
||||
},
|
||||
|
||||
initialize : function (options, ready) {
|
||||
var accountPath = options.accountNumber.slice(0,4) + '/' + options.accountNumber.slice(4);
|
||||
load('//dnn506yrbagrg.cloudfront.net/pages/scripts/'+accountPath+'.js?'+Math.floor(new Date().getTime()/3600000), ready);
|
||||
}
|
||||
|
||||
});
|
||||
});
|
||||
require.register("analytics/src/providers/customerio.js", function(exports, require, module){
|
||||
// http://customer.io/docs/api/javascript.html
|
||||
|
||||
var Provider = require('../provider')
|
||||
, isEmail = require('is-email')
|
||||
, load = require('load-script');
|
||||
|
||||
|
||||
module.exports = Provider.extend({
|
||||
|
||||
name : 'Customer.io',
|
||||
|
||||
key : 'siteId',
|
||||
|
||||
defaults : {
|
||||
siteId : null
|
||||
},
|
||||
|
||||
initialize : function (options, ready) {
|
||||
var _cio = window._cio = window._cio || [];
|
||||
(function() {
|
||||
var a,b,c;
|
||||
a = function (f) {
|
||||
return function () {
|
||||
_cio.push([f].concat(Array.prototype.slice.call(arguments,0)));
|
||||
};
|
||||
};
|
||||
b = ['identify', 'track'];
|
||||
for (c = 0; c < b.length; c++) {
|
||||
_cio[b[c]] = a(b[c]);
|
||||
}
|
||||
})();
|
||||
|
||||
// Load the Customer.io script and add the required `id` and `data-site-id`.
|
||||
var script = load('https://assets.customer.io/assets/track.js');
|
||||
script.id = 'cio-tracker';
|
||||
script.setAttribute('data-site-id', options.siteId);
|
||||
|
||||
// Since Customer.io creates their required methods in their snippet, we
|
||||
// don't need to wait to be ready.
|
||||
ready();
|
||||
},
|
||||
|
||||
identify : function (userId, traits) {
|
||||
// Don't do anything if we just have traits, because Customer.io
|
||||
// requires a `userId`.
|
||||
if (!userId) return;
|
||||
|
||||
// Customer.io takes the `userId` as part of the traits object.
|
||||
traits.id = userId;
|
||||
|
||||
// Swap the `created` trait to the `created_at` that Customer.io needs
|
||||
// and convert it from milliseconds to seconds.
|
||||
if (traits.created) {
|
||||
traits.created_at = Math.floor(traits.created/1000);
|
||||
delete traits.created;
|
||||
}
|
||||
|
||||
window._cio.identify(traits);
|
||||
},
|
||||
|
||||
track : function (event, properties) {
|
||||
window._cio.track(event, properties);
|
||||
}
|
||||
|
||||
});
|
||||
});
|
||||
require.register("analytics/src/providers/errorception.js", function(exports, require, module){
|
||||
// http://errorception.com/
|
||||
|
||||
var Provider = require('../provider')
|
||||
, extend = require('extend')
|
||||
, load = require('load-script')
|
||||
, type = require('type');
|
||||
|
||||
|
||||
module.exports = Provider.extend({
|
||||
|
||||
name : 'Errorception',
|
||||
|
||||
key : 'projectId',
|
||||
|
||||
defaults : {
|
||||
projectId : null,
|
||||
// Whether to store metadata about the user on `identify` calls, using
|
||||
// the [Errorception `meta` API](http://blog.errorception.com/2012/11/capture-custom-data-with-your-errors.html).
|
||||
meta : true
|
||||
},
|
||||
|
||||
initialize : function (options, ready) {
|
||||
window._errs = window._errs || [options.projectId];
|
||||
load('//d15qhc0lu1ghnk.cloudfront.net/beacon.js');
|
||||
|
||||
// Attach the window `onerror` event.
|
||||
var oldOnError = window.onerror;
|
||||
window.onerror = function () {
|
||||
window._errs.push(arguments);
|
||||
// Chain the old onerror handler after we finish our work.
|
||||
if ('function' === type(oldOnError)) {
|
||||
oldOnError.apply(this, arguments);
|
||||
}
|
||||
};
|
||||
|
||||
// Errorception makes a queue, so it's ready immediately.
|
||||
ready();
|
||||
},
|
||||
|
||||
// Add the traits to the Errorception meta object.
|
||||
identify : function (userId, traits) {
|
||||
if (!this.options.meta) return;
|
||||
|
||||
// If the custom metadata object hasn't ever been made, make it.
|
||||
window._errs.meta || (window._errs.meta = {});
|
||||
|
||||
// Add `userId` to traits.
|
||||
traits.id = userId;
|
||||
|
||||
// Add all of the traits as metadata.
|
||||
extend(window._errs.meta, traits);
|
||||
}
|
||||
|
||||
});
|
||||
});
|
||||
require.register("analytics/src/providers/foxmetrics.js", function(exports, require, module){
|
||||
// http://foxmetrics.com/documentation/apijavascript
|
||||
|
||||
var Provider = require('../provider')
|
||||
, load = require('load-script');
|
||||
|
||||
|
||||
module.exports = Provider.extend({
|
||||
|
||||
name : 'FoxMetrics',
|
||||
|
||||
key : 'appId',
|
||||
|
||||
defaults : {
|
||||
appId : null
|
||||
},
|
||||
|
||||
initialize : function (options, ready) {
|
||||
var _fxm = window._fxm || {};
|
||||
window._fxm = _fxm.events || [];
|
||||
load('//d35tca7vmefkrc.cloudfront.net/scripts/' + options.appId + '.js');
|
||||
|
||||
// FoxMetrics makes a queue, so it's ready immediately.
|
||||
ready();
|
||||
},
|
||||
|
||||
identify : function (userId, traits) {
|
||||
// A `userId` is required for profile updates.
|
||||
if (!userId) return;
|
||||
|
||||
// FoxMetrics needs the first and last name seperately. Fallback to
|
||||
// splitting the `name` trait if we don't have what we need.
|
||||
var firstName = traits.firstName
|
||||
, lastName = traits.lastName;
|
||||
|
||||
if (!firstName && traits.name) firstName = traits.name.split(' ')[0];
|
||||
if (!lastName && traits.name) lastName = traits.name.split(' ')[1];
|
||||
|
||||
window._fxm.push([
|
||||
'_fxm.visitor.profile',
|
||||
userId, // user id
|
||||
firstName, // first name
|
||||
lastName, // last name
|
||||
traits.email, // email
|
||||
traits.address, // address
|
||||
undefined, // social
|
||||
undefined, // partners
|
||||
traits // attributes
|
||||
]);
|
||||
},
|
||||
|
||||
track : function (event, properties) {
|
||||
window._fxm.push([
|
||||
event, // event name
|
||||
properties.category, // category
|
||||
properties // properties
|
||||
]);
|
||||
},
|
||||
|
||||
pageview : function (url) {
|
||||
window._fxm.push([
|
||||
'_fxm.pages.view',
|
||||
undefined, // title
|
||||
undefined, // name
|
||||
undefined, // category
|
||||
url, // url
|
||||
undefined // referrer
|
||||
]);
|
||||
}
|
||||
|
||||
});
|
||||
});
|
||||
require.register("analytics/src/providers/gauges.js", function(exports, require, module){
|
||||
// http://get.gaug.es/documentation/tracking/
|
||||
|
||||
var Provider = require('../provider')
|
||||
, load = require('load-script');
|
||||
|
||||
|
||||
module.exports = Provider.extend({
|
||||
|
||||
name : 'Gauges',
|
||||
|
||||
key : 'siteId',
|
||||
|
||||
defaults : {
|
||||
siteId : null
|
||||
},
|
||||
|
||||
initialize : function (options, ready) {
|
||||
window._gauges = window._gauges || [];
|
||||
var script = load('//secure.gaug.es/track.js');
|
||||
// Gauges needs a few attributes on its script element.
|
||||
script.id = 'gauges-tracker';
|
||||
script.setAttribute('data-site-id', options.siteId);
|
||||
|
||||
// Gauges make a queue so it's ready immediately.
|
||||
ready();
|
||||
},
|
||||
|
||||
pageview : function (url) {
|
||||
window._gauges.push(['track']);
|
||||
}
|
||||
|
||||
});
|
||||
});
|
||||
require.register("analytics/src/providers/get-satisfaction.js", function(exports, require, module){
|
||||
// You have to be signed in to access the snippet code:
|
||||
// https://console.getsatisfaction.com/start/101022?signup=true#engage
|
||||
|
||||
var Provider = require('../provider')
|
||||
, load = require('load-script')
|
||||
, onBody = require('on-body');
|
||||
|
||||
|
||||
module.exports = Provider.extend({
|
||||
|
||||
name : 'Get Satisfaction',
|
||||
|
||||
key : 'widgetId',
|
||||
|
||||
defaults : {
|
||||
widgetId : null
|
||||
},
|
||||
|
||||
initialize : function (options, ready) {
|
||||
// Get Satisfaction requires a div that will become their widget tab. Append
|
||||
// it once `document.body` exists.
|
||||
var div = document.createElement('div');
|
||||
var id = div.id = 'getsat-widget-' + options.widgetId;
|
||||
onBody(function (body) {
|
||||
body.appendChild(div);
|
||||
});
|
||||
|
||||
// Usually they load their snippet synchronously, so we need to wait for it
|
||||
// to come back before initializing the tab.
|
||||
load('https://loader.engage.gsfn.us/loader.js', function () {
|
||||
if (window.GSFN !== undefined) {
|
||||
window.GSFN.loadWidget(options.widgetId, { containerId : id });
|
||||
}
|
||||
ready();
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
});
|
||||
require.register("analytics/src/providers/google-analytics.js", function(exports, require, module){
|
||||
// https://developers.google.com/analytics/devguides/collection/gajs/
|
||||
|
||||
var Provider = require('../provider')
|
||||
, load = require('load-script')
|
||||
, type = require('type')
|
||||
, url = require('url')
|
||||
, canonical = require('canonical');
|
||||
|
||||
|
||||
module.exports = Provider.extend({
|
||||
|
||||
name : 'Google Analytics',
|
||||
|
||||
key : 'trackingId',
|
||||
|
||||
defaults : {
|
||||
// Whether to anonymize the IP address collected for the user.
|
||||
anonymizeIp : false,
|
||||
// An optional domain setting, to restrict where events can originate from.
|
||||
domain : null,
|
||||
// Whether to enable GOogle's DoubleClick remarketing feature.
|
||||
doubleClick : false,
|
||||
// Whether to use Google Analytics's Enhanced Link Attribution feature:
|
||||
// http://support.google.com/analytics/bin/answer.py?hl=en&answer=2558867
|
||||
enhancedLinkAttribution : false,
|
||||
// A domain to ignore for referrers. Maps to _addIgnoredRef
|
||||
ignoreReferrer : null,
|
||||
// Whether or not to track and initial pageview when initialized.
|
||||
initialPageview : true,
|
||||
// The setting to use for Google Analytics's Site Speed Sample Rate feature:
|
||||
// https://developers.google.com/analytics/devguides/collection/gajs/methods/gaJSApiBasicConfiguration#_gat.GA_Tracker_._setSiteSpeedSampleRate
|
||||
siteSpeedSampleRate : null,
|
||||
// Your Google Analytics Tracking ID.
|
||||
trackingId : null,
|
||||
// Whether you're using the new Universal Analytics or not.
|
||||
universalClient: false
|
||||
},
|
||||
|
||||
initialize : function (options, ready) {
|
||||
if (options.universalClient) this.initializeUniversal(options, ready);
|
||||
else this.initializeClassic(options, ready);
|
||||
},
|
||||
|
||||
initializeClassic: function (options, ready) {
|
||||
window._gaq = window._gaq || [];
|
||||
window._gaq.push(['_setAccount', options.trackingId]);
|
||||
|
||||
// Apply a bunch of optional settings.
|
||||
if (options.domain) {
|
||||
window._gaq.push(['_setDomainName', options.domain]);
|
||||
}
|
||||
if (options.enhancedLinkAttribution) {
|
||||
var protocol = 'https:' === document.location.protocol ? 'https:' : 'http:';
|
||||
var pluginUrl = protocol + '//www.google-analytics.com/plugins/ga/inpage_linkid.js';
|
||||
window._gaq.push(['_require', 'inpage_linkid', pluginUrl]);
|
||||
}
|
||||
if (type(options.siteSpeedSampleRate) === 'number') {
|
||||
window._gaq.push(['_setSiteSpeedSampleRate', options.siteSpeedSampleRate]);
|
||||
}
|
||||
if (options.anonymizeIp) {
|
||||
window._gaq.push(['_gat._anonymizeIp']);
|
||||
}
|
||||
if (options.ignoreReferrer) {
|
||||
window._gaq.push(['_addIgnoredRef', options.ignoreReferrer]);
|
||||
}
|
||||
if (options.initialPageview) {
|
||||
var path, canon = canonical();
|
||||
if (canon) path = url.parse(canon).pathname;
|
||||
this.pageview(path);
|
||||
}
|
||||
|
||||
// URLs change if DoubleClick is on. Even though Google Analytics makes a
|
||||
// queue, the `_gat` object isn't available until the library loads.
|
||||
if (options.doubleClick) {
|
||||
load('//stats.g.doubleclick.net/dc.js', ready);
|
||||
} else {
|
||||
load({
|
||||
http : 'http://www.google-analytics.com/ga.js',
|
||||
https : 'https://ssl.google-analytics.com/ga.js'
|
||||
}, ready);
|
||||
}
|
||||
},
|
||||
|
||||
initializeUniversal: function (options, ready) {
|
||||
|
||||
// GA-universal lets you set your own queue name
|
||||
var global = this.global = 'ga';
|
||||
|
||||
// and needs to know about this queue name in this special object
|
||||
// so that future plugins can also operate on the object
|
||||
window['GoogleAnalyticsObject'] = global;
|
||||
|
||||
// setup the global variable
|
||||
window[global] = window[global] || function () {
|
||||
(window[global].q = window[global].q || []).push(arguments);
|
||||
};
|
||||
|
||||
// GA also needs to know the current time (all from their snippet)
|
||||
window[global].l = 1 * new Date();
|
||||
|
||||
var createOpts = {};
|
||||
|
||||
// Apply a bunch of optional settings.
|
||||
if (options.domain)
|
||||
createOpts.cookieDomain = options.domain || 'none';
|
||||
if (type(options.siteSpeedSampleRate) === 'number')
|
||||
createOpts.siteSpeedSampleRate = options.siteSpeedSampleRate;
|
||||
if (options.anonymizeIp)
|
||||
ga('set', 'anonymizeIp', true);
|
||||
|
||||
ga('create', options.trackingId, createOpts);
|
||||
|
||||
if (options.initialPageview) {
|
||||
var path, canon = canonical();
|
||||
if (canon) path = url.parse(canon).pathname;
|
||||
this.pageview(path);
|
||||
}
|
||||
|
||||
load('//www.google-analytics.com/analytics.js');
|
||||
|
||||
// Google makes a queue so it's ready immediately.
|
||||
ready();
|
||||
},
|
||||
|
||||
track : function (event, properties) {
|
||||
properties || (properties = {});
|
||||
|
||||
var value;
|
||||
|
||||
// Since value is a common property name, ensure it is a number and Google
|
||||
// requires that it be an integer.
|
||||
if (type(properties.value) === 'number') value = Math.round(properties.value);
|
||||
|
||||
// Try to check for a `category` and `label`. A `category` is required,
|
||||
// so if it's not there we use `'All'` as a default. We can safely push
|
||||
// undefined if the special properties don't exist. Try using revenue
|
||||
// first, but fall back to a generic `value` as well.
|
||||
if (this.options.universalClient) {
|
||||
var opts = {};
|
||||
if (properties.noninteraction) opts.nonInteraction = properties.noninteraction;
|
||||
window[this.global](
|
||||
'send',
|
||||
'event',
|
||||
properties.category || 'All',
|
||||
event,
|
||||
properties.label,
|
||||
Math.round(properties.revenue) || value,
|
||||
opts
|
||||
);
|
||||
} else {
|
||||
window._gaq.push([
|
||||
'_trackEvent',
|
||||
properties.category || 'All',
|
||||
event,
|
||||
properties.label,
|
||||
Math.round(properties.revenue) || value,
|
||||
properties.noninteraction
|
||||
]);
|
||||
}
|
||||
},
|
||||
|
||||
pageview : function (url) {
|
||||
if (this.options.universalClient) {
|
||||
window[this.global]('send', 'pageview', url);
|
||||
} else {
|
||||
window._gaq.push(['_trackPageview', url]);
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
});
|
||||
require.register("analytics/src/providers/gosquared.js", function(exports, require, module){
|
||||
// http://www.gosquared.com/support
|
||||
// https://www.gosquared.com/customer/portal/articles/612063-tracker-functions
|
||||
|
||||
var Provider = require('../provider')
|
||||
, user = require('../user')
|
||||
, load = require('load-script')
|
||||
, onBody = require('on-body');
|
||||
|
||||
|
||||
module.exports = Provider.extend({
|
||||
|
||||
name : 'GoSquared',
|
||||
|
||||
key : 'siteToken',
|
||||
|
||||
defaults : {
|
||||
siteToken : null
|
||||
},
|
||||
|
||||
initialize : function (options, ready) {
|
||||
// GoSquared assumes a body in their script, so we need this wrapper.
|
||||
onBody(function () {
|
||||
var GoSquared = window.GoSquared = {};
|
||||
GoSquared.acct = options.siteToken;
|
||||
GoSquared.q = [];
|
||||
window._gstc_lt =+ (new Date());
|
||||
|
||||
GoSquared.VisitorName = user.id();
|
||||
GoSquared.Visitor = user.traits();
|
||||
|
||||
load('//d1l6p2sc9645hc.cloudfront.net/tracker.js');
|
||||
|
||||
// GoSquared makes a queue, so it's ready immediately.
|
||||
ready();
|
||||
});
|
||||
},
|
||||
|
||||
identify : function (userId, traits) {
|
||||
// TODO figure out if this will actually work. Seems like GoSquared will
|
||||
// never know these values are updated.
|
||||
if (userId) window.GoSquared.UserName = userId;
|
||||
if (traits) window.GoSquared.Visitor = traits;
|
||||
},
|
||||
|
||||
track : function (event, properties) {
|
||||
// GoSquared sets a `gs_evt_name` property with a value of the event
|
||||
// name, so it relies on properties being an object.
|
||||
window.GoSquared.q.push(['TrackEvent', event, properties || {}]);
|
||||
},
|
||||
|
||||
pageview : function (url) {
|
||||
window.GoSquared.q.push(['TrackView', url]);
|
||||
}
|
||||
|
||||
});
|
||||
});
|
||||
require.register("analytics/src/providers/heap.js", function(exports, require, module){
|
||||
// https://heapanalytics.com/docs
|
||||
|
||||
var Provider = require('../provider')
|
||||
, load = require('load-script');
|
||||
|
||||
module.exports = Provider.extend({
|
||||
|
||||
name : 'Heap',
|
||||
|
||||
key : 'apiKey',
|
||||
|
||||
defaults : {
|
||||
apiKey : null
|
||||
},
|
||||
|
||||
initialize : function (options, ready) {
|
||||
window.heap=window.heap||[];window.heap.load=function(a){window._heapid=a;var b=document.createElement("script");b.type="text/javascript",b.async=!0,b.src=("https:"===document.location.protocol?"https:":"http:")+"//d36lvucg9kzous.cloudfront.net";var c=document.getElementsByTagName("script")[0];c.parentNode.insertBefore(b,c);var d=function(a){return function(){heap.push([a].concat(Array.prototype.slice.call(arguments,0)))}},e=["identify","track"];for(var f=0;f<e.length;f++)heap[e[f]]=d(e[f])};
|
||||
window.heap.load(options.apiKey);
|
||||
|
||||
// heap creates its own queue, so we're ready right away
|
||||
ready();
|
||||
},
|
||||
|
||||
identify : function (userId, traits) {
|
||||
window.heap.identify(traits);
|
||||
},
|
||||
|
||||
track : function (event, properties) {
|
||||
window.heap.track(event, properties);
|
||||
}
|
||||
|
||||
});
|
||||
});
|
||||
require.register("analytics/src/providers/hittail.js", function(exports, require, module){
|
||||
// http://www.hittail.com
|
||||
|
||||
var Provider = require('../provider')
|
||||
, load = require('load-script');
|
||||
|
||||
|
||||
module.exports = Provider.extend({
|
||||
|
||||
name : 'HitTail',
|
||||
|
||||
key : 'siteId',
|
||||
|
||||
defaults : {
|
||||
siteId : null
|
||||
},
|
||||
|
||||
initialize : function (options, ready) {
|
||||
load('//' + options.siteId + '.hittail.com/mlt.js', ready);
|
||||
}
|
||||
|
||||
});
|
||||
});
|
||||
require.register("analytics/src/providers/hubspot.js", function(exports, require, module){
|
||||
// http://hubspot.clarify-it.com/d/4m62hl
|
||||
|
||||
var Provider = require('../provider')
|
||||
, isEmail = require('is-email')
|
||||
, load = require('load-script');
|
||||
|
||||
|
||||
module.exports = Provider.extend({
|
||||
|
||||
name : 'HubSpot',
|
||||
|
||||
key : 'portalId',
|
||||
|
||||
defaults : {
|
||||
portalId : null
|
||||
},
|
||||
|
||||
initialize : function (options, ready) {
|
||||
// HubSpot checks in their snippet to make sure another script with
|
||||
// `hs-analytics` isn't already in the DOM. Seems excessive, but who knows
|
||||
// if there's weird deprecation going on :p
|
||||
if (!document.getElementById('hs-analytics')) {
|
||||
window._hsq = window._hsq || [];
|
||||
var script = load('https://js.hubspot.com/analytics/' + (Math.ceil(new Date()/300000)*300000) + '/' + options.portalId + '.js');
|
||||
script.id = 'hs-analytics';
|
||||
}
|
||||
|
||||
// HubSpot makes a queue, so it's ready immediately.
|
||||
ready();
|
||||
},
|
||||
|
||||
// HubSpot does not use a userId, but the email address is required on
|
||||
// the traits object.
|
||||
identify : function (userId, traits) {
|
||||
window._hsq.push(["identify", traits]);
|
||||
},
|
||||
|
||||
// Event Tracking is available to HubSpot Enterprise customers only. In
|
||||
// addition to adding any unique event name, you can also use the id of an
|
||||
// existing custom event as the event variable.
|
||||
track : function (event, properties) {
|
||||
window._hsq.push(["trackEvent", event, properties]);
|
||||
},
|
||||
|
||||
// HubSpot doesn't support passing in a custom URL.
|
||||
pageview : function (url) {
|
||||
window._hsq.push(['_trackPageview']);
|
||||
}
|
||||
|
||||
});
|
||||
});
|
||||
require.register("analytics/src/providers/index.js", function(exports, require, module){
|
||||
module.exports = [
|
||||
require('./adroll'),
|
||||
require('./amplitude'),
|
||||
require('./bitdeli'),
|
||||
require('./bugherd'),
|
||||
require('./chartbeat'),
|
||||
require('./clicktale'),
|
||||
require('./clicky'),
|
||||
require('./comscore'),
|
||||
require('./crazyegg'),
|
||||
require('./customerio'),
|
||||
require('./errorception'),
|
||||
require('./foxmetrics'),
|
||||
require('./gauges'),
|
||||
require('./get-satisfaction'),
|
||||
require('./google-analytics'),
|
||||
require('./gosquared'),
|
||||
require('./heap'),
|
||||
require('./hittail'),
|
||||
require('./hubspot'),
|
||||
require('./improvely'),
|
||||
require('./intercom'),
|
||||
require('./keen-io'),
|
||||
require('./kissmetrics'),
|
||||
require('./klaviyo'),
|
||||
require('./livechat'),
|
||||
require('./lytics'),
|
||||
require('./mixpanel'),
|
||||
require('./olark'),
|
||||
require('./optimizely'),
|
||||
require('./perfect-audience'),
|
||||
require('./pingdom'),
|
||||
require('./preact'),
|
||||
require('./qualaroo'),
|
||||
require('./quantcast'),
|
||||
require('./sentry'),
|
||||
require('./snapengage'),
|
||||
require('./usercycle'),
|
||||
require('./userfox'),
|
||||
require('./uservoice'),
|
||||
require('./vero'),
|
||||
require('./visual-website-optimizer'),
|
||||
require('./woopra')
|
||||
];
|
||||
|
||||
});
|
||||
require.register("analytics/src/providers/improvely.js", function(exports, require, module){
|
||||
// http://www.improvely.com/docs/landing-page-code
|
||||
// http://www.improvely.com/docs/conversion-code
|
||||
// http://www.improvely.com/docs/labeling-visitors
|
||||
|
||||
var Provider = require('../provider')
|
||||
, alias = require('alias')
|
||||
, load = require('load-script');
|
||||
|
||||
|
||||
module.exports = Provider.extend({
|
||||
|
||||
name : 'Improvely',
|
||||
|
||||
defaults : {
|
||||
// Improvely requires two options: `domain` and `projectId`.
|
||||
domain : null,
|
||||
projectId : null
|
||||
},
|
||||
|
||||
initialize : function (options, ready) {
|
||||
window._improvely = window._improvely || [];
|
||||
window.improvely = window.improvely || {
|
||||
init : function (e, t) { window._improvely.push(["init", e, t]); },
|
||||
goal : function (e) { window._improvely.push(["goal", e]); },
|
||||
label : function (e) { window._improvely.push(["label", e]); }
|
||||
};
|
||||
|
||||
load('//' + options.domain + '.iljmp.com/improvely.js');
|
||||
window.improvely.init(options.domain, options.projectId);
|
||||
|
||||
// Improvely creates a queue, so it's ready immediately.
|
||||
ready();
|
||||
},
|
||||
|
||||
identify : function (userId, traits) {
|
||||
if (userId) window.improvely.label(userId);
|
||||
},
|
||||
|
||||
track : function (event, properties) {
|
||||
// Improvely calls `revenue` `amount`, and puts the `event` in properties as
|
||||
// the `type`.
|
||||
properties || (properties = {});
|
||||
properties.type = event;
|
||||
alias(properties, { 'revenue' : 'amount' });
|
||||
window.improvely.goal(properties);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
require.register("analytics/src/providers/intercom.js", function(exports, require, module){
|
||||
// http://docs.intercom.io/
|
||||
// http://docs.intercom.io/#IntercomJS
|
||||
|
||||
var Provider = require('../provider')
|
||||
, extend = require('extend')
|
||||
, load = require('load-script')
|
||||
, isEmail = require('is-email');
|
||||
|
||||
|
||||
module.exports = Provider.extend({
|
||||
|
||||
name : 'Intercom',
|
||||
|
||||
// Whether Intercom has already been booted or not. Intercom becomes booted
|
||||
// after Intercom('boot', ...) has been called on the first identify.
|
||||
booted : false,
|
||||
|
||||
key : 'appId',
|
||||
|
||||
defaults : {
|
||||
// Intercom's required key.
|
||||
appId : null,
|
||||
// An optional setting to display the Intercom inbox widget.
|
||||
activator : null,
|
||||
// Whether to show the count of messages for the inbox widget.
|
||||
counter : true
|
||||
},
|
||||
|
||||
initialize : function (options, ready) {
|
||||
load('https://static.intercomcdn.com/intercom.v1.js', ready);
|
||||
},
|
||||
|
||||
identify : function (userId, traits, options) {
|
||||
// Don't do anything if we just have traits the first time.
|
||||
if (!this.booted && !userId) return;
|
||||
|
||||
// Intercom specific settings. BACKWARDS COMPATIBILITY: we need to check for
|
||||
// the lowercase variant as well.
|
||||
options || (options = {});
|
||||
var Intercom = options.Intercom || options.intercom || {};
|
||||
traits.increments = Intercom.increments;
|
||||
traits.user_hash = Intercom.userHash || Intercom.user_hash;
|
||||
|
||||
// They need `created_at` as a Unix timestamp (seconds).
|
||||
if (traits.created) {
|
||||
traits.created_at = Math.floor(traits.created/1000);
|
||||
delete traits.created;
|
||||
}
|
||||
|
||||
// Convert a `company`'s `created` date.
|
||||
if (traits.company && traits.company.created) {
|
||||
traits.company.created_at = Math.floor(traits.company.created/1000);
|
||||
delete traits.company.created;
|
||||
}
|
||||
|
||||
// Optionally add the inbox widget.
|
||||
if (this.options.activator) {
|
||||
traits.widget = {
|
||||
activator : this.options.activator,
|
||||
use_counter : this.options.counter
|
||||
};
|
||||
}
|
||||
|
||||
// If this is the first time we've identified, `boot` instead of `update`
|
||||
// and add our one-time boot settings.
|
||||
if (this.booted) {
|
||||
window.Intercom('update', traits);
|
||||
} else {
|
||||
extend(traits, {
|
||||
app_id : this.options.appId,
|
||||
user_id : userId
|
||||
});
|
||||
window.Intercom('boot', traits);
|
||||
}
|
||||
|
||||
// Set the booted state, so that we know to call 'update' next time.
|
||||
this.booted = true;
|
||||
},
|
||||
|
||||
// Intercom doesn't have a separate `group` method, but they take a
|
||||
// `companies` trait for the user.
|
||||
group : function (groupId, properties, options) {
|
||||
properties.id = groupId;
|
||||
window.Intercom('update', { company : properties });
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
require.register("analytics/src/providers/keen-io.js", function(exports, require, module){
|
||||
// https://keen.io/docs/
|
||||
|
||||
var Provider = require('../provider')
|
||||
, load = require('load-script');
|
||||
|
||||
|
||||
module.exports = Provider.extend({
|
||||
|
||||
name : 'Keen IO',
|
||||
|
||||
defaults : {
|
||||
// The Project ID is **required**.
|
||||
projectId : null,
|
||||
// The Write Key is **required** to send events.
|
||||
writeKey : null,
|
||||
// The Read Key is optional, only if you want to "do analysis".
|
||||
readKey : null,
|
||||
// Whether or not to pass pageviews on to Keen IO.
|
||||
pageview : true,
|
||||
// Whether or not to track an initial pageview on `initialize`.
|
||||
initialPageview : true
|
||||
},
|
||||
|
||||
initialize : function (options, ready) {
|
||||
window.Keen = window.Keen||{configure:function(e){this._cf=e},addEvent:function(e,t,n,i){this._eq=this._eq||[],this._eq.push([e,t,n,i])},setGlobalProperties:function(e){this._gp=e},onChartsReady:function(e){this._ocrq=this._ocrq||[],this._ocrq.push(e)}};
|
||||
window.Keen.configure({
|
||||
projectId : options.projectId,
|
||||
writeKey : options.writeKey,
|
||||
readKey : options.readKey
|
||||
});
|
||||
|
||||
load('//dc8na2hxrj29i.cloudfront.net/code/keen-2.1.0-min.js');
|
||||
|
||||
if (options.initialPageview) this.pageview();
|
||||
|
||||
// Keen IO defines all their functions in the snippet, so they're ready.
|
||||
ready();
|
||||
},
|
||||
|
||||
identify : function (userId, traits) {
|
||||
// Use Keen IO global properties to include `userId` and `traits` on
|
||||
// every event sent to Keen IO.
|
||||
var globalUserProps = {};
|
||||
if (userId) globalUserProps.userId = userId;
|
||||
if (traits) globalUserProps.traits = traits;
|
||||
if (userId || traits) {
|
||||
window.Keen.setGlobalProperties(function(eventCollection) {
|
||||
return { user: globalUserProps };
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
track : function (event, properties) {
|
||||
window.Keen.addEvent(event, properties);
|
||||
},
|
||||
|
||||
pageview : function (url) {
|
||||
if (!this.options.pageview) return;
|
||||
|
||||
var properties = {
|
||||
url : url || document.location.href,
|
||||
name : document.title
|
||||
};
|
||||
|
||||
this.track('Loaded a Page', properties);
|
||||
}
|
||||
|
||||
});
|
||||
});
|
||||
require.register("analytics/src/providers/kissmetrics.js", function(exports, require, module){
|
||||
// http://support.kissmetrics.com/apis/javascript
|
||||
|
||||
var Provider = require('../provider')
|
||||
, alias = require('alias')
|
||||
, load = require('load-script');
|
||||
|
||||
|
||||
module.exports = Provider.extend({
|
||||
|
||||
name : 'KISSmetrics',
|
||||
|
||||
key : 'apiKey',
|
||||
|
||||
defaults : {
|
||||
apiKey : null
|
||||
},
|
||||
|
||||
initialize : function (options, ready) {
|
||||
window._kmq = window._kmq || [];
|
||||
load('//i.kissmetrics.com/i.js');
|
||||
load('//doug1izaerwt3.cloudfront.net/' + options.apiKey + '.1.js');
|
||||
|
||||
// KISSmetrics creates a queue, so it's ready immediately.
|
||||
ready();
|
||||
},
|
||||
|
||||
// KISSmetrics uses two separate methods: `identify` for storing the
|
||||
// `userId`, and `set` for storing `traits`.
|
||||
identify : function (userId, traits) {
|
||||
if (userId) window._kmq.push(['identify', userId]);
|
||||
if (traits) window._kmq.push(['set', traits]);
|
||||
},
|
||||
|
||||
track : function (event, properties) {
|
||||
// KISSmetrics handles revenue with the `'Billing Amount'` property by
|
||||
// default, although it's changeable in the interface.
|
||||
if (properties) {
|
||||
alias(properties, {
|
||||
'revenue' : 'Billing Amount'
|
||||
});
|
||||
}
|
||||
|
||||
window._kmq.push(['record', event, properties]);
|
||||
},
|
||||
|
||||
// Although undocumented, KISSmetrics actually supports not passing a second
|
||||
// ID, in which case it uses the currenty identified user's ID.
|
||||
alias : function (newId, originalId) {
|
||||
window._kmq.push(['alias', newId, originalId]);
|
||||
}
|
||||
|
||||
});
|
||||
});
|
||||
require.register("analytics/src/providers/klaviyo.js", function(exports, require, module){
|
||||
// https://www.klaviyo.com/docs
|
||||
|
||||
var Provider = require('../provider')
|
||||
, load = require('load-script');
|
||||
|
||||
|
||||
module.exports = Provider.extend({
|
||||
|
||||
name : 'Klaviyo',
|
||||
|
||||
key : 'apiKey',
|
||||
|
||||
defaults : {
|
||||
apiKey : null
|
||||
},
|
||||
|
||||
initialize : function (options, ready) {
|
||||
window._learnq = window._learnq || [];
|
||||
window._learnq.push(['account', options.apiKey]);
|
||||
load('//a.klaviyo.com/media/js/learnmarklet.js');
|
||||
|
||||
// Klaviyo creats a queue, so it's ready immediately.
|
||||
ready();
|
||||
},
|
||||
|
||||
identify : function (userId, traits) {
|
||||
// Klaviyo requires a `userId` and takes the it on the traits object itself.
|
||||
if (!userId) return;
|
||||
traits.$id = userId;
|
||||
window._learnq.push(['identify', traits]);
|
||||
},
|
||||
|
||||
track : function (event, properties) {
|
||||
window._learnq.push(['track', event, properties]);
|
||||
}
|
||||
|
||||
});
|
||||
});
|
||||
require.register("analytics/src/providers/livechat.js", function(exports, require, module){
|
||||
// http://www.livechatinc.com/api/javascript-api
|
||||
|
||||
var Provider = require('../provider')
|
||||
, each = require('each')
|
||||
, load = require('load-script');
|
||||
|
||||
|
||||
module.exports = Provider.extend({
|
||||
|
||||
name : 'LiveChat',
|
||||
|
||||
key : 'license',
|
||||
|
||||
defaults : {
|
||||
license : null
|
||||
},
|
||||
|
||||
initialize : function (options, ready) {
|
||||
window.__lc = { license : options.license };
|
||||
load('//cdn.livechatinc.com/tracking.js', ready);
|
||||
},
|
||||
|
||||
// LiveChat isn't an analytics service, but we can use the `userId` and
|
||||
// `traits` to tag the user with their real name in the chat console.
|
||||
identify : function (userId, traits) {
|
||||
// In case the LiveChat library hasn't loaded yet.
|
||||
if (!window.LC_API) return;
|
||||
|
||||
// LiveChat takes them in an array format.
|
||||
var variables = [];
|
||||
|
||||
if (userId) variables.push({ name: 'User ID', value: userId });
|
||||
if (traits) {
|
||||
each(traits, function (key, value) {
|
||||
variables.push({
|
||||
name : key,
|
||||
value : value
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
window.LC_API.set_custom_variables(variables);
|
||||
}
|
||||
|
||||
});
|
||||
});
|
||||
require.register("analytics/src/providers/lytics.js", function(exports, require, module){
|
||||
// Lytics
|
||||
// --------
|
||||
// [Documentation](http://developer.lytics.io/doc#jstag),
|
||||
|
||||
var Provider = require('../provider')
|
||||
, load = require('load-script');
|
||||
|
||||
|
||||
module.exports = Provider.extend({
|
||||
|
||||
name : 'Lytics',
|
||||
|
||||
key : 'cid',
|
||||
|
||||
defaults : {
|
||||
cid: null
|
||||
},
|
||||
|
||||
initialize : function (options, ready) {
|
||||
window.jstag = (function () {
|
||||
var t={_q:[],_c:{cid:options.cid,url:'//c.lytics.io'},ts:(new Date()).getTime()};
|
||||
t.send=function(){
|
||||
this._q.push(["ready","send",Array.prototype.slice.call(arguments)]);
|
||||
return this;
|
||||
};
|
||||
return t;
|
||||
})();
|
||||
|
||||
load('//c.lytics.io/static/io.min.js');
|
||||
|
||||
ready();
|
||||
},
|
||||
|
||||
identify: function (userId, traits) {
|
||||
traits._uid = userId;
|
||||
window.jstag.send(traits);
|
||||
},
|
||||
|
||||
track: function (event, properties) {
|
||||
properties._e = event;
|
||||
window.jstag.send(properties);
|
||||
},
|
||||
|
||||
pageview: function (url) {
|
||||
window.jstag.send();
|
||||
}
|
||||
|
||||
});
|
||||
});
|
||||
require.register("analytics/src/providers/mixpanel.js", function(exports, require, module){
|
||||
// https://mixpanel.com/docs/integration-libraries/javascript
|
||||
// https://mixpanel.com/docs/people-analytics/javascript
|
||||
// https://mixpanel.com/docs/integration-libraries/javascript-full-api
|
||||
|
||||
var Provider = require('../provider')
|
||||
, alias = require('alias')
|
||||
, isEmail = require('is-email')
|
||||
, load = require('load-script');
|
||||
|
||||
|
||||
module.exports = Provider.extend({
|
||||
|
||||
name : 'Mixpanel',
|
||||
|
||||
key : 'token',
|
||||
|
||||
defaults : {
|
||||
// Whether to call `mixpanel.nameTag` on `identify`.
|
||||
nameTag : true,
|
||||
// Whether to use Mixpanel's People API.
|
||||
people : false,
|
||||
// The Mixpanel API token for your account.
|
||||
token : null,
|
||||
// Whether to track pageviews to Mixpanel.
|
||||
pageview : false,
|
||||
// Whether to track an initial pageview on initialize.
|
||||
initialPageview : false
|
||||
},
|
||||
|
||||
initialize : function (options, ready) {
|
||||
(function (c, a) {
|
||||
window.mixpanel = a;
|
||||
var b, d, h, e;
|
||||
a._i = [];
|
||||
a.init = function (b, c, f) {
|
||||
function d(a, b) {
|
||||
var c = b.split('.');
|
||||
2 == c.length && (a = a[c[0]], b = c[1]);
|
||||
a[b] = function () {
|
||||
a.push([b].concat(Array.prototype.slice.call(arguments, 0)));
|
||||
};
|
||||
}
|
||||
var g = a;
|
||||
'undefined' !== typeof f ? g = a[f] = [] : f = 'mixpanel';
|
||||
g.people = g.people || [];
|
||||
h = ['disable', 'track', 'track_pageview', 'track_links', 'track_forms', 'register', 'register_once', 'unregister', 'identify', 'alias', 'name_tag', 'set_config', 'people.set', 'people.increment', 'people.track_charge', 'people.append'];
|
||||
for (e = 0; e < h.length; e++) d(g, h[e]);
|
||||
a._i.push([b, c, f]);
|
||||
};
|
||||
a.__SV = 1.2;
|
||||
// Modification to the snippet: call ready whenever the library has
|
||||
// fully loaded.
|
||||
load('//cdn.mxpnl.com/libs/mixpanel-2.2.min.js', ready);
|
||||
})(document, window.mixpanel || []);
|
||||
|
||||
// Pass options directly to `init` as the second argument.
|
||||
window.mixpanel.init(options.token, options);
|
||||
|
||||
if (options.initialPageview) this.pageview();
|
||||
},
|
||||
|
||||
identify : function (userId, traits) {
|
||||
// Alias the traits' keys with dollar signs for Mixpanel's API.
|
||||
alias(traits, {
|
||||
'created' : '$created',
|
||||
'email' : '$email',
|
||||
'firstName' : '$first_name',
|
||||
'lastName' : '$last_name',
|
||||
'lastSeen' : '$last_seen',
|
||||
'name' : '$name',
|
||||
'username' : '$username',
|
||||
'phone' : '$phone'
|
||||
});
|
||||
|
||||
// Finally, call all of the identify equivalents. Verify certain calls
|
||||
// against options to make sure they're enabled.
|
||||
if (userId) {
|
||||
window.mixpanel.identify(userId);
|
||||
if (this.options.nameTag) window.mixpanel.name_tag(traits && traits.$email || userId);
|
||||
}
|
||||
if (traits) {
|
||||
window.mixpanel.register(traits);
|
||||
if (this.options.people) window.mixpanel.people.set(traits);
|
||||
}
|
||||
},
|
||||
|
||||
track : function (event, properties) {
|
||||
window.mixpanel.track(event, properties);
|
||||
|
||||
// Mixpanel handles revenue with a `transaction` call in their People
|
||||
// feature. So if we're using people, record a transcation.
|
||||
if (properties && properties.revenue && this.options.people) {
|
||||
window.mixpanel.people.track_charge(properties.revenue);
|
||||
}
|
||||
},
|
||||
|
||||
// Mixpanel doesn't actually track the pageviews, but they do show up in the
|
||||
// Mixpanel stream.
|
||||
pageview : function (url) {
|
||||
window.mixpanel.track_pageview(url);
|
||||
|
||||
// If they don't want pageviews tracked, leave now.
|
||||
if (!this.options.pageview) return;
|
||||
|
||||
var properties = {
|
||||
url : url || document.location.href,
|
||||
name : document.title
|
||||
};
|
||||
|
||||
this.track('Loaded a Page', properties);
|
||||
},
|
||||
|
||||
// Although undocumented, Mixpanel actually supports the `originalId`. It
|
||||
// just usually defaults to the current user's `distinct_id`.
|
||||
alias : function (newId, originalId) {
|
||||
|
||||
if(window.mixpanel.get_distinct_id &&
|
||||
window.mixpanel.get_distinct_id() === newId) return;
|
||||
|
||||
// HACK: internal mixpanel API to ensure we don't overwrite.
|
||||
if(window.mixpanel.get_property &&
|
||||
window.mixpanel.get_property('$people_distinct_id') === newId) return;
|
||||
|
||||
window.mixpanel.alias(newId, originalId);
|
||||
}
|
||||
|
||||
});
|
||||
});
|
||||
require.register("analytics/src/providers/olark.js", function(exports, require, module){
|
||||
// http://www.olark.com/documentation
|
||||
|
||||
var Provider = require('../provider')
|
||||
, isEmail = require('is-email');
|
||||
|
||||
|
||||
module.exports = Provider.extend({
|
||||
|
||||
name : 'Olark',
|
||||
|
||||
key : 'siteId',
|
||||
|
||||
chatting : false,
|
||||
|
||||
defaults : {
|
||||
siteId : null,
|
||||
// Whether to use the user's name or email in the Olark chat console.
|
||||
identify : true,
|
||||
// Whether to log pageviews to the Olark chat console.
|
||||
track : false,
|
||||
// Whether to log pageviews to the Olark chat console.
|
||||
pageview : true
|
||||
},
|
||||
|
||||
initialize : function (options, ready) {
|
||||
window.olark||(function(c){var f=window,d=document,l=f.location.protocol=="https:"?"https:":"http:",z=c.name,r="load";var nt=function(){f[z]=function(){(a.s=a.s||[]).push(arguments)};var a=f[z]._={},q=c.methods.length;while(q--){(function(n){f[z][n]=function(){f[z]("call",n,arguments)}})(c.methods[q])}a.l=c.loader;a.i=nt;a.p={0:+new Date};a.P=function(u){a.p[u]=new Date-a.p[0]};function s(){a.P(r);f[z](r)}f.addEventListener?f.addEventListener(r,s,false):f.attachEvent("on"+r,s);var ld=function(){function p(hd){hd="head";return["<",hd,"></",hd,"><",i,' onl' + 'oad="var d=',g,";d.getElementsByTagName('head')[0].",j,"(d.",h,"('script')).",k,"='",l,"//",a.l,"'",'"',"></",i,">"].join("")}var i="body",m=d[i];if(!m){return setTimeout(ld,100)}a.P(1);var j="appendChild",h="createElement",k="src",n=d[h]("div"),v=n[j](d[h](z)),b=d[h]("iframe"),g="document",e="domain",o;n.style.display="none";m.insertBefore(n,m.firstChild).id=z;b.frameBorder="0";b.id=z+"-loader";if(/MSIE[ ]+6/.test(navigator.userAgent)){b.src="javascript:false"}b.allowTransparency="true";v[j](b);try{b.contentWindow[g].open()}catch(w){c[e]=d[e];o="javascript:var d="+g+".open();d.domain='"+d.domain+"';";b[k]=o+"void(0);"}try{var t=b.contentWindow[g];t.write(p());t.close()}catch(x){b[k]=o+'d.write("'+p().replace(/"/g,String.fromCharCode(92)+'"')+'");d.close();'}a.P(2)};ld()};nt()})({loader: "static.olark.com/jsclient/loader0.js",name:"olark",methods:["configure","extend","declare","identify"]});
|
||||
window.olark.identify(options.siteId);
|
||||
|
||||
// Set up event handlers for chat box open and close so that
|
||||
// we know whether a conversation is active. If it is active,
|
||||
// then we'll send track and pageview information.
|
||||
var self = this;
|
||||
window.olark('api.box.onExpand', function () { self.chatting = true; });
|
||||
window.olark('api.box.onShrink', function () { self.chatting = false; });
|
||||
|
||||
// Olark creates it's method in the snippet, so it's ready immediately.
|
||||
ready();
|
||||
},
|
||||
|
||||
// Update traits about the user in Olark to make the operator's life easier.
|
||||
identify : function (userId, traits) {
|
||||
if (!this.options.identify) return;
|
||||
|
||||
var email = traits.email
|
||||
, name = traits.name || traits.firstName
|
||||
, phone = traits.phone
|
||||
, nickname = name || email || userId;
|
||||
|
||||
// If we have a name and an email, add the email too to be more helpful.
|
||||
if (name && email) nickname += ' ('+email+')';
|
||||
|
||||
// Call all of Olark's settings APIs.
|
||||
window.olark('api.visitor.updateCustomFields', traits);
|
||||
if (email) window.olark('api.visitor.updateEmailAddress', { emailAddress : email });
|
||||
if (name) window.olark('api.visitor.updateFullName', { fullName : name });
|
||||
if (phone) window.olark('api.visitor.updatePhoneNumber', { phoneNumber : phone });
|
||||
if (nickname) window.olark('api.chat.updateVisitorNickname', { snippet : nickname });
|
||||
},
|
||||
|
||||
// Log events the user triggers to the chat console, if you so desire it.
|
||||
track : function (event, properties) {
|
||||
if (!this.options.track || !this.chatting) return;
|
||||
|
||||
// To stay consistent with olark's default messages, it's all lowercase.
|
||||
window.olark('api.chat.sendNotificationToOperator', {
|
||||
body : 'visitor triggered "'+event+'"'
|
||||
});
|
||||
},
|
||||
|
||||
// Mimic the functionality Olark has for normal pageviews with pseudo-
|
||||
// pageviews, telling the operator when a visitor changes pages.
|
||||
pageview : function (url) {
|
||||
if (!this.options.pageview || !this.chatting) return;
|
||||
|
||||
// To stay consistent with olark's default messages, it's all lowercase.
|
||||
window.olark('api.chat.sendNotificationToOperator', {
|
||||
body : 'looking at ' + window.location.href
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
});
|
||||
require.register("analytics/src/providers/optimizely.js", function(exports, require, module){
|
||||
// https://www.optimizely.com/docs/api
|
||||
|
||||
var each = require('each')
|
||||
, nextTick = require('next-tick')
|
||||
, Provider = require('../provider');
|
||||
|
||||
|
||||
module.exports = Provider.extend({
|
||||
|
||||
name : 'Optimizely',
|
||||
|
||||
defaults : {
|
||||
// Whether to replay variations into other enabled integrations as traits.
|
||||
variations : true
|
||||
},
|
||||
|
||||
initialize : function (options, ready, analytics) {
|
||||
// Create the `optimizely` object in case it doesn't exist already.
|
||||
// https://www.optimizely.com/docs/api#function-calls
|
||||
window.optimizely = window.optimizely || [];
|
||||
|
||||
// If the `variations` option is true, replay our variations on the next
|
||||
// tick to wait for the entire library to be ready for replays.
|
||||
if (options.variations) {
|
||||
var self = this;
|
||||
nextTick(function () { self.replay(); });
|
||||
}
|
||||
|
||||
// Optimizely should be on the page already, so it's always ready.
|
||||
ready();
|
||||
},
|
||||
|
||||
track : function (event, properties) {
|
||||
// Optimizely takes revenue as cents, not dollars.
|
||||
if (properties && properties.revenue) properties.revenue = properties.revenue * 100;
|
||||
|
||||
window.optimizely.push(['trackEvent', event, properties]);
|
||||
},
|
||||
|
||||
replay : function () {
|
||||
// Make sure we have access to Optimizely's `data` dictionary.
|
||||
var data = window.optimizely.data;
|
||||
if (!data) return;
|
||||
|
||||
// Grab a few pieces of data we'll need for replaying.
|
||||
var experiments = data.experiments
|
||||
, variationNamesMap = data.state.variationNamesMap;
|
||||
|
||||
// Create our traits object to add variations to.
|
||||
var traits = {};
|
||||
|
||||
// Loop through all the experiement the user has been assigned a variation
|
||||
// for and add them to our traits.
|
||||
each(variationNamesMap, function (experimentId, variation) {
|
||||
traits['Experiment: ' + experiments[experimentId].name] = variation;
|
||||
});
|
||||
|
||||
this.analytics.identify(traits);
|
||||
}
|
||||
|
||||
});
|
||||
});
|
||||
require.register("analytics/src/providers/perfect-audience.js", function(exports, require, module){
|
||||
// https://www.perfectaudience.com/docs#javascript_api_autoopen
|
||||
|
||||
var Provider = require('../provider')
|
||||
, load = require('load-script');
|
||||
|
||||
|
||||
module.exports = Provider.extend({
|
||||
|
||||
name : 'Perfect Audience',
|
||||
|
||||
key : 'siteId',
|
||||
|
||||
defaults : {
|
||||
siteId : null
|
||||
},
|
||||
|
||||
initialize : function (options, ready) {
|
||||
window._pa || (window._pa = {});
|
||||
load('//tag.perfectaudience.com/serve/' + options.siteId + '.js', ready);
|
||||
},
|
||||
|
||||
track : function (event, properties) {
|
||||
window._pa.track(event, properties);
|
||||
}
|
||||
|
||||
});
|
||||
});
|
||||
require.register("analytics/src/providers/pingdom.js", function(exports, require, module){
|
||||
var date = require('load-date')
|
||||
, Provider = require('../provider')
|
||||
, load = require('load-script');
|
||||
|
||||
|
||||
module.exports = Provider.extend({
|
||||
|
||||
name : 'Pingdom',
|
||||
|
||||
key : 'id',
|
||||
|
||||
defaults : {
|
||||
id : null
|
||||
},
|
||||
|
||||
initialize : function (options, ready) {
|
||||
|
||||
window._prum = [
|
||||
['id', options.id],
|
||||
['mark', 'firstbyte', date.getTime()]
|
||||
];
|
||||
|
||||
// We've replaced the original snippet loader with our own load method.
|
||||
load('//rum-static.pingdom.net/prum.min.js', ready);
|
||||
}
|
||||
|
||||
});
|
||||
});
|
||||
require.register("analytics/src/providers/preact.js", function(exports, require, module){
|
||||
// http://www.preact.io/api/javascript
|
||||
|
||||
var Provider = require('../provider')
|
||||
, isEmail = require('is-email')
|
||||
, load = require('load-script');
|
||||
|
||||
module.exports = Provider.extend({
|
||||
|
||||
name : 'Preact',
|
||||
|
||||
key : 'projectCode',
|
||||
|
||||
defaults : {
|
||||
projectCode : null
|
||||
},
|
||||
|
||||
initialize : function (options, ready) {
|
||||
var _lnq = window._lnq = window._lnq || [];
|
||||
_lnq.push(["_setCode", options.projectCode]);
|
||||
|
||||
load('//d2bbvl6dq48fa6.cloudfront.net/js/ln-2.4.min.js');
|
||||
ready();
|
||||
},
|
||||
|
||||
identify : function (userId, traits) {
|
||||
// Don't do anything if we just have traits. Preact requires a `userId`.
|
||||
if (!userId) return;
|
||||
|
||||
// Swap the `created` trait to the `created_at` that Preact needs
|
||||
// and convert it from milliseconds to seconds.
|
||||
if (traits.created) {
|
||||
traits.created_at = Math.floor(traits.created/1000);
|
||||
delete traits.created;
|
||||
}
|
||||
|
||||
window._lnq.push(['_setPersonData', {
|
||||
name : traits.name,
|
||||
email : traits.email,
|
||||
uid : userId,
|
||||
properties : traits
|
||||
}]);
|
||||
},
|
||||
|
||||
group : function (groupId, properties) {
|
||||
if (!groupId) return;
|
||||
properties.id = groupId;
|
||||
window._lnq.push(['_setAccount', properties]);
|
||||
},
|
||||
|
||||
track : function (event, properties) {
|
||||
properties || (properties = {});
|
||||
|
||||
// Preact takes a few special properties, and the rest in `extras`. So first
|
||||
// convert and remove the special ones from `properties`.
|
||||
var special = { name : event };
|
||||
|
||||
// They take `revenue` in cents.
|
||||
if (properties.revenue) {
|
||||
special.revenue = properties.revenue * 100;
|
||||
delete properties.revenue;
|
||||
}
|
||||
|
||||
if (properties.note) {
|
||||
special.note = properties.note;
|
||||
delete properties.note;
|
||||
}
|
||||
|
||||
window._lnq.push(['_logEvent', special, properties]);
|
||||
}
|
||||
|
||||
});
|
||||
});
|
||||
require.register("analytics/src/providers/qualaroo.js", function(exports, require, module){
|
||||
// http://help.qualaroo.com/customer/portal/articles/731085-identify-survey-nudge-takers
|
||||
// http://help.qualaroo.com/customer/portal/articles/731091-set-additional-user-properties
|
||||
|
||||
var Provider = require('../provider')
|
||||
, isEmail = require('is-email')
|
||||
, load = require('load-script');
|
||||
|
||||
|
||||
module.exports = Provider.extend({
|
||||
|
||||
name : 'Qualaroo',
|
||||
|
||||
defaults : {
|
||||
// Qualaroo has two required options.
|
||||
customerId : null,
|
||||
siteToken : null,
|
||||
// Whether to record traits when a user triggers an event. This can be
|
||||
// useful for sending targetted questionnaries.
|
||||
track : false
|
||||
},
|
||||
|
||||
// Qualaroo's script has two options in its URL.
|
||||
initialize : function (options, ready) {
|
||||
window._kiq = window._kiq || [];
|
||||
load('//s3.amazonaws.com/ki.js/' + options.customerId + '/' + options.siteToken + '.js');
|
||||
|
||||
// Qualaroo creates a queue, so it's ready immediately.
|
||||
ready();
|
||||
},
|
||||
|
||||
// Qualaroo uses two separate methods: `identify` for storing the `userId`,
|
||||
// and `set` for storing `traits`.
|
||||
identify : function (userId, traits) {
|
||||
var identity = traits.email || userId;
|
||||
if (identity) window._kiq.push(['identify', identity]);
|
||||
if (traits) window._kiq.push(['set', traits]);
|
||||
},
|
||||
|
||||
// Qualaroo doesn't have `track` method yet, but to allow the users to do
|
||||
// targetted questionnaires we can set name-value pairs on the user properties
|
||||
// that apply to the current visit.
|
||||
track : function (event, properties) {
|
||||
if (!this.options.track) return;
|
||||
|
||||
// Create a name-value pair that will be pretty unique. For an event like
|
||||
// 'Loaded a Page' this will make it 'Triggered: Loaded a Page'.
|
||||
var traits = {};
|
||||
traits['Triggered: ' + event] = true;
|
||||
|
||||
// Fire a normal identify, with traits only.
|
||||
this.identify(null, traits);
|
||||
}
|
||||
|
||||
});
|
||||
});
|
||||
require.register("analytics/src/providers/quantcast.js", function(exports, require, module){
|
||||
// https://www.quantcast.com/learning-center/guides/using-the-quantcast-asynchronous-tag/
|
||||
|
||||
var Provider = require('../provider')
|
||||
, load = require('load-script');
|
||||
|
||||
|
||||
module.exports = Provider.extend({
|
||||
|
||||
name : 'Quantcast',
|
||||
|
||||
key : 'pCode',
|
||||
|
||||
defaults : {
|
||||
pCode : null
|
||||
},
|
||||
|
||||
initialize : function (options, ready) {
|
||||
window._qevents = window._qevents || [];
|
||||
window._qevents.push({ qacct: options.pCode });
|
||||
load({
|
||||
http : 'http://edge.quantserve.com/quant.js',
|
||||
https : 'https://secure.quantserve.com/quant.js'
|
||||
}, ready);
|
||||
}
|
||||
|
||||
});
|
||||
});
|
||||
require.register("analytics/src/providers/sentry.js", function(exports, require, module){
|
||||
// http://raven-js.readthedocs.org/en/latest/config/index.html
|
||||
|
||||
var Provider = require('../provider')
|
||||
, load = require('load-script');
|
||||
|
||||
|
||||
module.exports = Provider.extend({
|
||||
|
||||
name : 'Sentry',
|
||||
|
||||
key : 'config',
|
||||
|
||||
defaults : {
|
||||
config : null
|
||||
},
|
||||
|
||||
initialize : function (options, ready) {
|
||||
load('//d3nslu0hdya83q.cloudfront.net/dist/1.0/raven.min.js', function () {
|
||||
// For now, Raven basically requires `install` to be called.
|
||||
// https://github.com/getsentry/raven-js/blob/master/src/raven.js#L87
|
||||
window.Raven.config(options.config).install();
|
||||
ready();
|
||||
});
|
||||
},
|
||||
|
||||
identify : function (userId, traits) {
|
||||
traits.id = userId;
|
||||
window.Raven.setUser(traits);
|
||||
},
|
||||
|
||||
// Raven will automatically use `captureMessage` if the error is a string.
|
||||
log : function (error, properties) {
|
||||
window.Raven.captureException(error, properties);
|
||||
}
|
||||
|
||||
});
|
||||
});
|
||||
require.register("analytics/src/providers/snapengage.js", function(exports, require, module){
|
||||
// http://help.snapengage.com/installation-guide-getting-started-in-a-snap/
|
||||
|
||||
var Provider = require('../provider')
|
||||
, isEmail = require('is-email')
|
||||
, load = require('load-script');
|
||||
|
||||
|
||||
module.exports = Provider.extend({
|
||||
|
||||
name : 'SnapEngage',
|
||||
|
||||
key : 'apiKey',
|
||||
|
||||
defaults : {
|
||||
apiKey : null
|
||||
},
|
||||
|
||||
initialize : function (options, ready) {
|
||||
load('//commondatastorage.googleapis.com/code.snapengage.com/js/' + options.apiKey + '.js', ready);
|
||||
},
|
||||
|
||||
// Set the email in the chat window if we have it.
|
||||
identify : function (userId, traits, options) {
|
||||
if (!traits.email) return;
|
||||
window.SnapABug.setUserEmail(traits.email);
|
||||
}
|
||||
|
||||
});
|
||||
});
|
||||
require.register("analytics/src/providers/usercycle.js", function(exports, require, module){
|
||||
// http://docs.usercycle.com/javascript_api
|
||||
|
||||
var Provider = require('../provider')
|
||||
, load = require('load-script')
|
||||
, user = require('../user');
|
||||
|
||||
|
||||
module.exports = Provider.extend({
|
||||
|
||||
name : 'USERcycle',
|
||||
|
||||
key : 'key',
|
||||
|
||||
defaults : {
|
||||
key : null
|
||||
},
|
||||
|
||||
initialize : function (options, ready) {
|
||||
window._uc = window._uc || [];
|
||||
window._uc.push(['_key', options.key]);
|
||||
load('//api.usercycle.com/javascripts/track.js');
|
||||
|
||||
// USERcycle makes a queue, so it's ready immediately.
|
||||
ready();
|
||||
},
|
||||
|
||||
identify : function (userId, traits) {
|
||||
if (userId) window._uc.push(['uid', userId]);
|
||||
|
||||
// USERcycle has a special "hidden" event that is used just for retention measurement.
|
||||
// Lukas suggested on 6/4/2013 that we send traits on that event, since they use the
|
||||
// the latest value of every event property as a "trait"
|
||||
window._uc.push(['action', 'came_back', traits]);
|
||||
},
|
||||
|
||||
track : function (event, properties) {
|
||||
window._uc.push(['action', event, properties]);
|
||||
}
|
||||
|
||||
});
|
||||
});
|
||||
require.register("analytics/src/providers/userfox.js", function(exports, require, module){
|
||||
// https://www.userfox.com/docs/
|
||||
|
||||
var Provider = require('../provider')
|
||||
, extend = require('extend')
|
||||
, load = require('load-script')
|
||||
, isEmail = require('is-email');
|
||||
|
||||
|
||||
module.exports = Provider.extend({
|
||||
|
||||
name : 'userfox',
|
||||
|
||||
key : 'clientId',
|
||||
|
||||
defaults : {
|
||||
// userfox's required key.
|
||||
clientId : null
|
||||
},
|
||||
|
||||
initialize : function (options, ready) {
|
||||
window._ufq = window._ufq || [];
|
||||
load('//d2y71mjhnajxcg.cloudfront.net/js/userfox-stable.js');
|
||||
|
||||
// userfox creates its own queue, so we're ready right away.
|
||||
ready();
|
||||
},
|
||||
|
||||
identify : function (userId, traits) {
|
||||
if (!traits.email) return;
|
||||
|
||||
// Initialize the library with the email now that we have it.
|
||||
window._ufq.push(['init', {
|
||||
clientId : this.options.clientId,
|
||||
email : traits.email
|
||||
}]);
|
||||
|
||||
// Record traits to "track" if we have the required signup date `created`.
|
||||
// userfox takes `signup_date` as a string of seconds since the epoch.
|
||||
if (traits.created) {
|
||||
traits.signup_date = (traits.created.getTime() / 1000).toString();
|
||||
delete traits.created;
|
||||
window._ufq.push(['track', traits]);
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
require.register("analytics/src/providers/uservoice.js", function(exports, require, module){
|
||||
// http://feedback.uservoice.com/knowledgebase/articles/225-how-do-i-pass-custom-data-through-the-widget-and-i
|
||||
|
||||
var Provider = require('../provider')
|
||||
, load = require('load-script')
|
||||
, alias = require('alias')
|
||||
, clone = require('clone');
|
||||
|
||||
|
||||
module.exports = Provider.extend({
|
||||
|
||||
name : 'UserVoice',
|
||||
|
||||
defaults : {
|
||||
// These first two options are required.
|
||||
widgetId : null,
|
||||
forumId : null,
|
||||
// Should we show the tab automatically?
|
||||
showTab : true,
|
||||
// There's tons of options for the tab.
|
||||
mode : 'full',
|
||||
primaryColor : '#cc6d00',
|
||||
linkColor : '#007dbf',
|
||||
defaultMode : 'support',
|
||||
tabLabel : 'Feedback & Support',
|
||||
tabColor : '#cc6d00',
|
||||
tabPosition : 'middle-right',
|
||||
tabInverted : false
|
||||
},
|
||||
|
||||
initialize : function (options, ready) {
|
||||
window.UserVoice = window.UserVoice || [];
|
||||
load('//widget.uservoice.com/' + options.widgetId + '.js', ready);
|
||||
|
||||
var optionsClone = clone(options);
|
||||
alias(optionsClone, {
|
||||
'forumId' : 'forum_id',
|
||||
'primaryColor' : 'primary_color',
|
||||
'linkColor' : 'link_color',
|
||||
'defaultMode' : 'default_mode',
|
||||
'tabLabel' : 'tab_label',
|
||||
'tabColor' : 'tab_color',
|
||||
'tabPosition' : 'tab_position',
|
||||
'tabInverted' : 'tab_inverted'
|
||||
});
|
||||
|
||||
// If we don't automatically show the tab, let them show it via
|
||||
// javascript. This is the default name for the function in their snippet.
|
||||
window.showClassicWidget = function (showWhat) {
|
||||
window.UserVoice.push([showWhat || 'showLightbox', 'classic_widget', optionsClone]);
|
||||
};
|
||||
|
||||
// If we *do* automatically show the tab, get on with it!
|
||||
if (options.showTab) {
|
||||
window.showClassicWidget('showTab');
|
||||
}
|
||||
},
|
||||
|
||||
identify : function (userId, traits) {
|
||||
// Pull the ID into traits.
|
||||
traits.id = userId;
|
||||
window.UserVoice.push(['setCustomFields', traits]);
|
||||
}
|
||||
|
||||
});
|
||||
});
|
||||
require.register("analytics/src/providers/vero.js", function(exports, require, module){
|
||||
// https://github.com/getvero/vero-api/blob/master/sections/js.md
|
||||
|
||||
var Provider = require('../provider')
|
||||
, isEmail = require('is-email')
|
||||
, load = require('load-script');
|
||||
|
||||
|
||||
module.exports = Provider.extend({
|
||||
|
||||
name : 'Vero',
|
||||
|
||||
key : 'apiKey',
|
||||
|
||||
defaults : {
|
||||
apiKey : null
|
||||
},
|
||||
|
||||
initialize : function (options, ready) {
|
||||
window._veroq = window._veroq || [];
|
||||
window._veroq.push(['init', { api_key: options.apiKey }]);
|
||||
load('//d3qxef4rp70elm.cloudfront.net/m.js');
|
||||
|
||||
// Vero creates a queue, so it's ready immediately.
|
||||
ready();
|
||||
},
|
||||
|
||||
identify : function (userId, traits) {
|
||||
// Don't do anything if we just have traits, because Vero
|
||||
// requires a `userId`.
|
||||
if (!userId || !traits.email) return;
|
||||
|
||||
// Vero takes the `userId` as part of the traits object.
|
||||
traits.id = userId;
|
||||
|
||||
window._veroq.push(['user', traits]);
|
||||
},
|
||||
|
||||
track : function (event, properties) {
|
||||
window._veroq.push(['track', event, properties]);
|
||||
}
|
||||
|
||||
});
|
||||
});
|
||||
require.register("analytics/src/providers/visual-website-optimizer.js", function(exports, require, module){
|
||||
// http://v2.visualwebsiteoptimizer.com/tools/get_tracking_code.php
|
||||
// http://visualwebsiteoptimizer.com/knowledge/integration-of-vwo-with-kissmetrics/
|
||||
|
||||
var each = require('each')
|
||||
, inherit = require('inherit')
|
||||
, nextTick = require('next-tick')
|
||||
, Provider = require('../provider');
|
||||
|
||||
|
||||
/**
|
||||
* Expose `VWO`.
|
||||
*/
|
||||
|
||||
module.exports = VWO;
|
||||
|
||||
|
||||
/**
|
||||
* `VWO` inherits from the generic `Provider`.
|
||||
*/
|
||||
|
||||
function VWO () {
|
||||
Provider.apply(this, arguments);
|
||||
}
|
||||
|
||||
inherit(VWO, Provider);
|
||||
|
||||
|
||||
/**
|
||||
* Name.
|
||||
*/
|
||||
|
||||
VWO.prototype.name = 'Visual Website Optimizer';
|
||||
|
||||
|
||||
/**
|
||||
* Default options.
|
||||
*/
|
||||
|
||||
VWO.prototype.defaults = {
|
||||
// Whether to replay variations into other integrations as traits.
|
||||
replay : true
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Initialize.
|
||||
*/
|
||||
|
||||
VWO.prototype.initialize = function (options, ready) {
|
||||
if (options.replay) this.replay();
|
||||
ready();
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Replay the experiments the user has seen as traits to all other integrations.
|
||||
* Wait for the next tick to replay so that the `analytics` object and all of
|
||||
* the integrations are fully initialized.
|
||||
*/
|
||||
|
||||
VWO.prototype.replay = function () {
|
||||
var analytics = this.analytics;
|
||||
nextTick(function () {
|
||||
experiments(function (err, traits) {
|
||||
if (traits) analytics.identify(traits);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Get dictionary of experiment keys and variations.
|
||||
* http://visualwebsiteoptimizer.com/knowledge/integration-of-vwo-with-kissmetrics/
|
||||
*
|
||||
* @param {Function} callback Called with `err, experiments`.
|
||||
* @return {Object} Dictionary of experiments and variations.
|
||||
*/
|
||||
|
||||
function experiments (callback) {
|
||||
enqueue(function () {
|
||||
var data = {};
|
||||
var ids = window._vwo_exp_ids;
|
||||
if (!ids) return callback();
|
||||
each(ids, function (id) {
|
||||
var name = variation(id);
|
||||
if (name) data['Experiment: ' + id] = name;
|
||||
});
|
||||
callback(null, data);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Add a function to the VWO queue, creating one if it doesn't exist.
|
||||
*
|
||||
* @param {Function} fn Function to enqueue.
|
||||
*/
|
||||
|
||||
function enqueue (fn) {
|
||||
window._vis_opt_queue || (window._vis_opt_queue = []);
|
||||
window._vis_opt_queue.push(fn);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the chosen variation's name from an experiment `id`.
|
||||
* http://visualwebsiteoptimizer.com/knowledge/integration-of-vwo-with-kissmetrics/
|
||||
*
|
||||
* @param {String} id ID of the experiment to read.
|
||||
* @return {String} Variation name.
|
||||
*/
|
||||
|
||||
function variation (id) {
|
||||
var experiments = window._vwo_exp;
|
||||
if (!experiments) return null;
|
||||
var experiment = experiments[id];
|
||||
var variationId = experiment.combination_chosen;
|
||||
return variationId ? experiment.comb_n[variationId] : null;
|
||||
}
|
||||
});
|
||||
require.register("analytics/src/providers/woopra.js", function(exports, require, module){
|
||||
// http://www.woopra.com/docs/setup/javascript-tracking/
|
||||
|
||||
var Provider = require('../provider')
|
||||
, each = require('each')
|
||||
, extend = require('extend')
|
||||
, isEmail = require('is-email')
|
||||
, load = require('load-script')
|
||||
, type = require('type')
|
||||
, user = require('../user');
|
||||
|
||||
|
||||
module.exports = Provider.extend({
|
||||
|
||||
name : 'Woopra',
|
||||
|
||||
key : 'domain',
|
||||
|
||||
defaults : {
|
||||
domain : null
|
||||
},
|
||||
|
||||
initialize : function (options, ready) {
|
||||
// Woopra gives us a nice ready callback.
|
||||
var self = this;
|
||||
|
||||
window.woopraReady = function (tracker) {
|
||||
tracker.setDomain(self.options.domain);
|
||||
tracker.setIdleTimeout(300000);
|
||||
|
||||
var userId = user.id()
|
||||
, traits = user.traits();
|
||||
|
||||
addTraits(userId, traits, tracker);
|
||||
tracker.track();
|
||||
|
||||
ready();
|
||||
return false;
|
||||
};
|
||||
|
||||
load('//static.woopra.com/js/woopra.js');
|
||||
},
|
||||
|
||||
identify : function (userId, traits) {
|
||||
// We aren't guaranteed a tracker.
|
||||
if (!window.woopraTracker) return;
|
||||
addTraits(userId, traits, window.woopraTracker);
|
||||
},
|
||||
|
||||
track : function (event, properties) {
|
||||
// We aren't guaranteed a tracker.
|
||||
if (!window.woopraTracker) return;
|
||||
|
||||
// Woopra takes its `event` as the `name` key.
|
||||
properties || (properties = {});
|
||||
properties.name = event;
|
||||
|
||||
window.woopraTracker.pushEvent(properties);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* Convenience function for updating the userId and traits.
|
||||
*
|
||||
* @param {String} userId The user's ID.
|
||||
* @param {Object} traits The user's traits.
|
||||
* @param {Tracker} tracker The Woopra tracker object.
|
||||
*/
|
||||
|
||||
function addTraits (userId, traits, tracker) {
|
||||
// Move a `userId` into `traits`.
|
||||
if (userId) traits.id = userId;
|
||||
each(traits, function (key, value) {
|
||||
// Woopra seems to only support strings as trait values.
|
||||
if ('string' === type(value)) tracker.addVisitorProperty(key, value);
|
||||
});
|
||||
}
|
||||
});
|
||||
require.alias("avetisk-defaults/index.js", "analytics/deps/defaults/index.js");
|
||||
require.alias("avetisk-defaults/index.js", "defaults/index.js");
|
||||
|
||||
require.alias("component-clone/index.js", "analytics/deps/clone/index.js");
|
||||
require.alias("component-clone/index.js", "clone/index.js");
|
||||
require.alias("component-type/index.js", "component-clone/deps/type/index.js");
|
||||
|
||||
require.alias("component-cookie/index.js", "analytics/deps/cookie/index.js");
|
||||
require.alias("component-cookie/index.js", "cookie/index.js");
|
||||
|
||||
require.alias("component-each/index.js", "analytics/deps/each/index.js");
|
||||
require.alias("component-each/index.js", "each/index.js");
|
||||
require.alias("component-type/index.js", "component-each/deps/type/index.js");
|
||||
|
||||
require.alias("component-event/index.js", "analytics/deps/event/index.js");
|
||||
require.alias("component-event/index.js", "event/index.js");
|
||||
|
||||
require.alias("component-inherit/index.js", "analytics/deps/inherit/index.js");
|
||||
require.alias("component-inherit/index.js", "inherit/index.js");
|
||||
|
||||
require.alias("component-object/index.js", "analytics/deps/object/index.js");
|
||||
require.alias("component-object/index.js", "object/index.js");
|
||||
|
||||
require.alias("component-querystring/index.js", "analytics/deps/querystring/index.js");
|
||||
require.alias("component-querystring/index.js", "querystring/index.js");
|
||||
require.alias("component-trim/index.js", "component-querystring/deps/trim/index.js");
|
||||
|
||||
require.alias("component-type/index.js", "analytics/deps/type/index.js");
|
||||
require.alias("component-type/index.js", "type/index.js");
|
||||
|
||||
require.alias("component-url/index.js", "analytics/deps/url/index.js");
|
||||
require.alias("component-url/index.js", "url/index.js");
|
||||
|
||||
require.alias("segmentio-after/index.js", "analytics/deps/after/index.js");
|
||||
require.alias("segmentio-after/index.js", "after/index.js");
|
||||
|
||||
require.alias("segmentio-alias/index.js", "analytics/deps/alias/index.js");
|
||||
require.alias("segmentio-alias/index.js", "alias/index.js");
|
||||
|
||||
require.alias("segmentio-bind-all/index.js", "analytics/deps/bind-all/index.js");
|
||||
require.alias("segmentio-bind-all/index.js", "analytics/deps/bind-all/index.js");
|
||||
require.alias("segmentio-bind-all/index.js", "bind-all/index.js");
|
||||
require.alias("component-bind/index.js", "segmentio-bind-all/deps/bind/index.js");
|
||||
|
||||
require.alias("component-type/index.js", "segmentio-bind-all/deps/type/index.js");
|
||||
|
||||
require.alias("segmentio-bind-all/index.js", "segmentio-bind-all/index.js");
|
||||
|
||||
require.alias("segmentio-canonical/index.js", "analytics/deps/canonical/index.js");
|
||||
require.alias("segmentio-canonical/index.js", "canonical/index.js");
|
||||
|
||||
require.alias("segmentio-extend/index.js", "analytics/deps/extend/index.js");
|
||||
require.alias("segmentio-extend/index.js", "extend/index.js");
|
||||
|
||||
require.alias("segmentio-is-email/index.js", "analytics/deps/is-email/index.js");
|
||||
require.alias("segmentio-is-email/index.js", "is-email/index.js");
|
||||
|
||||
require.alias("segmentio-is-meta/index.js", "analytics/deps/is-meta/index.js");
|
||||
require.alias("segmentio-is-meta/index.js", "is-meta/index.js");
|
||||
|
||||
require.alias("segmentio-json/index.js", "analytics/deps/json/index.js");
|
||||
require.alias("segmentio-json/index.js", "json/index.js");
|
||||
require.alias("component-json-fallback/index.js", "segmentio-json/deps/json-fallback/index.js");
|
||||
|
||||
require.alias("segmentio-load-date/index.js", "analytics/deps/load-date/index.js");
|
||||
require.alias("segmentio-load-date/index.js", "load-date/index.js");
|
||||
|
||||
require.alias("segmentio-load-script/index.js", "analytics/deps/load-script/index.js");
|
||||
require.alias("segmentio-load-script/index.js", "load-script/index.js");
|
||||
require.alias("component-type/index.js", "segmentio-load-script/deps/type/index.js");
|
||||
|
||||
require.alias("segmentio-new-date/index.js", "analytics/deps/new-date/index.js");
|
||||
require.alias("segmentio-new-date/index.js", "new-date/index.js");
|
||||
require.alias("component-type/index.js", "segmentio-new-date/deps/type/index.js");
|
||||
|
||||
require.alias("segmentio-on-body/index.js", "analytics/deps/on-body/index.js");
|
||||
require.alias("segmentio-on-body/index.js", "on-body/index.js");
|
||||
require.alias("component-each/index.js", "segmentio-on-body/deps/each/index.js");
|
||||
require.alias("component-type/index.js", "component-each/deps/type/index.js");
|
||||
|
||||
require.alias("segmentio-store.js/store.js", "analytics/deps/store/store.js");
|
||||
require.alias("segmentio-store.js/store.js", "analytics/deps/store/index.js");
|
||||
require.alias("segmentio-store.js/store.js", "store/index.js");
|
||||
require.alias("segmentio-json/index.js", "segmentio-store.js/deps/json/index.js");
|
||||
require.alias("component-json-fallback/index.js", "segmentio-json/deps/json-fallback/index.js");
|
||||
|
||||
require.alias("segmentio-store.js/store.js", "segmentio-store.js/index.js");
|
||||
|
||||
require.alias("segmentio-top-domain/index.js", "analytics/deps/top-domain/index.js");
|
||||
require.alias("segmentio-top-domain/index.js", "analytics/deps/top-domain/index.js");
|
||||
require.alias("segmentio-top-domain/index.js", "top-domain/index.js");
|
||||
require.alias("component-url/index.js", "segmentio-top-domain/deps/url/index.js");
|
||||
|
||||
require.alias("segmentio-top-domain/index.js", "segmentio-top-domain/index.js");
|
||||
|
||||
require.alias("timoxley-next-tick/index.js", "analytics/deps/next-tick/index.js");
|
||||
require.alias("timoxley-next-tick/index.js", "next-tick/index.js");
|
||||
|
||||
require.alias("yields-prevent/index.js", "analytics/deps/prevent/index.js");
|
||||
require.alias("yields-prevent/index.js", "prevent/index.js");
|
||||
|
||||
require.alias("analytics/src/index.js", "analytics/index.js");
|
||||
|
||||
if (typeof exports == "object") {
|
||||
module.exports = require("analytics");
|
||||
} else if (typeof define == "function" && define.amd) {
|
||||
define(function(){ return require("analytics"); });
|
||||
} else {
|
||||
this["analytics"] = require("analytics");
|
||||
}})();
|
||||
1
common/static/js/vendor/underscore.string.min.js
vendored
Normal file
1
common/static/js/vendor/underscore.string.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -26,6 +26,8 @@
|
||||
<script type="text/javascript" src="<%= common_js_root %>/vendor/tiny_mce/tiny_mce.js"></script>
|
||||
<script type="text/javascript" src="<%= common_js_root %>/vendor/mathjax-MathJax-c9db6ac/MathJax.js?config=default"></script>
|
||||
<script type="text/javascript" src="<%= common_js_root %>/vendor/jquery.timeago.js"></script>
|
||||
<script type="text/javascript" src="<%= common_js_root %>/vendor/sinon-1.7.1.js"></script>
|
||||
<script type="text/javascript" src="<%= common_js_root %>/vendor/analytics.js"></script>
|
||||
<script type="text/javascript">
|
||||
AjaxPrefix.addAjaxPrefix(jQuery, function() {
|
||||
return "";
|
||||
|
||||
@@ -63,6 +63,25 @@ To get a full list of available rake tasks, use:
|
||||
|
||||
rake -T
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
#### Reference Error: XModule is not defined (javascript)
|
||||
This means that the javascript defining an xmodule hasn't loaded correctly. There are a number
|
||||
of different things that could be causing this:
|
||||
|
||||
1. See `Error: watch EMFILE`
|
||||
|
||||
#### Error: watch EMFILE (coffee)
|
||||
When running a development server, we also start a watcher process alongside to recompile coffeescript
|
||||
and sass as changes are made. On Mac OSX systems, the coffee watcher process takes more file handles
|
||||
than are allowed by default. This will result in `EMFILE` errors when coffeescript is running, and
|
||||
will prevent javascript from compiling, leading to the error 'XModule is not defined'
|
||||
|
||||
To work around this issue, we use `Process::setrlimit` to set the number of allowed open files.
|
||||
Coffee watches both directories and files, so you will need to set this fairly high (anecdotally,
|
||||
8000 seems to do the trick on OSX 10.7.5, 10.8.3, and 10.8.4)
|
||||
|
||||
|
||||
## Running Tests
|
||||
|
||||
See `testing.md` for instructions on running the test suite.
|
||||
|
||||
@@ -23,8 +23,11 @@ be specified for this tag::
|
||||
|
||||
sources - location id of required modules, separated by ';'
|
||||
[message | ""] - message for case, where one or more are not passed. Here you can use variable {link}, which generate link to required module.
|
||||
|
||||
[submitted] - map to `is_submitted` module method.
|
||||
(pressing RESET button makes this function to return False.)
|
||||
|
||||
[completed] - map to `is_completed` module method
|
||||
[correct] - map to `is_correct` module method
|
||||
[attempted] - map to `is_attempted` module method
|
||||
[poll_answer] - map to `poll_answer` module attribute
|
||||
[voted] - map to `voted` module attribute
|
||||
@@ -53,7 +56,7 @@ Examples of conditional depends on poll
|
||||
</conditional>
|
||||
|
||||
Examples of conditional depends on poll (use <show> tag)
|
||||
-------------------------------------------
|
||||
--------------------------------------------------------
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
|
||||
@@ -420,6 +420,6 @@ Draggables can be reused
|
||||
.. literalinclude:: drag-n-drop-demo2.xml
|
||||
|
||||
Examples of targets on draggables
|
||||
------------------------
|
||||
---------------------------------
|
||||
|
||||
.. literalinclude:: drag-n-drop-demo3.xml
|
||||
|
||||
@@ -362,7 +362,7 @@ that has to be updated on a parameter's change, then one can define
|
||||
a special function to handle this. The "output" of such a function must be
|
||||
set to "none", and the JavaScript code inside this function must update the
|
||||
MathJax element by itself. Before exiting, MathJax typeset function should
|
||||
be called so that the new text will be re-rendered by MathJax. For example,
|
||||
be called so that the new text will be re-rendered by MathJax. For example::
|
||||
|
||||
<render>
|
||||
...
|
||||
|
||||
@@ -19,11 +19,11 @@ This is a partial list of features, to be revised as we go along:
|
||||
|
||||
An example of a problem::
|
||||
|
||||
<symbolicresponse expect="a_b^c + b_x__d" size="30">
|
||||
<textline math="1"
|
||||
<symbolicresponse expect="a_b^c + b_x__d" size="30">
|
||||
<textline math="1"
|
||||
preprocessorClassName="SymbolicMathjaxPreprocessor"
|
||||
preprocessorSrc="/static/js/capa/symbolic_mathjax_preprocessor.js"/>
|
||||
</symbolicresponse>
|
||||
</symbolicresponse>
|
||||
|
||||
It's a bit of a pain to enter that.
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ Specific Problem Types
|
||||
course_data_formats/conditional_module/conditional_module.rst
|
||||
course_data_formats/word_cloud/word_cloud.rst
|
||||
course_data_formats/custom_response.rst
|
||||
course_data_formats/symbolic_response.rst
|
||||
|
||||
|
||||
Internal Data Formats
|
||||
|
||||
7
docs/source/calc.rst
Normal file
7
docs/source/calc.rst
Normal file
@@ -0,0 +1,7 @@
|
||||
*******************************************
|
||||
Calc
|
||||
*******************************************
|
||||
|
||||
.. automodule:: calc
|
||||
:members:
|
||||
:show-inheritance:
|
||||
@@ -8,14 +8,6 @@ Contents:
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
chem.rst
|
||||
|
||||
Calc
|
||||
====
|
||||
|
||||
.. automodule:: capa.calc
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
Capa_problem
|
||||
============
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
*******************************************
|
||||
Chem module
|
||||
Chemistry modules
|
||||
*******************************************
|
||||
|
||||
.. module:: chem
|
||||
@@ -7,7 +7,7 @@ Chem module
|
||||
Miller
|
||||
======
|
||||
|
||||
.. automodule:: capa.chem.miller
|
||||
.. automodule:: chem.miller
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
@@ -47,14 +47,14 @@ Documentation from **crystallography.js**::
|
||||
Chemcalc
|
||||
========
|
||||
|
||||
.. automodule:: capa.chem.chemcalc
|
||||
.. automodule:: chem.chemcalc
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
Chemtools
|
||||
=========
|
||||
|
||||
.. automodule:: capa.chem.chemtools
|
||||
.. automodule:: chem.chemtools
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
@@ -62,7 +62,7 @@ Chemtools
|
||||
Tests
|
||||
=====
|
||||
|
||||
.. automodule:: capa.chem.tests
|
||||
.. automodule:: chem.tests
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
|
||||
@@ -4,86 +4,3 @@ CMS module
|
||||
|
||||
.. module:: cms
|
||||
|
||||
Auth
|
||||
====
|
||||
|
||||
.. automodule:: auth
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
Authz
|
||||
-----
|
||||
|
||||
.. automodule:: auth.authz
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
Content store
|
||||
=============
|
||||
|
||||
.. .. automodule:: contentstore
|
||||
.. :members:
|
||||
.. :show-inheritance:
|
||||
|
||||
.. Utils
|
||||
.. -----
|
||||
|
||||
.. .. automodule:: contentstore.untils
|
||||
.. :members:
|
||||
.. :show-inheritance:
|
||||
|
||||
.. Views
|
||||
.. -----
|
||||
|
||||
.. .. automodule:: contentstore.views
|
||||
.. :members:
|
||||
.. :show-inheritance:
|
||||
|
||||
.. Management
|
||||
.. ----------
|
||||
|
||||
.. .. automodule:: contentstore.management
|
||||
.. :members:
|
||||
.. :show-inheritance:
|
||||
|
||||
.. Tests
|
||||
.. -----
|
||||
|
||||
.. .. automodule:: contentstore.tests
|
||||
.. :members:
|
||||
.. :show-inheritance:
|
||||
|
||||
Github sync
|
||||
===========
|
||||
|
||||
.. automodule:: github_sync
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
Exceptions
|
||||
----------
|
||||
|
||||
.. automodule:: github_sync.exceptions
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
Views
|
||||
-----
|
||||
|
||||
.. automodule:: github_sync.views
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
Management
|
||||
----------
|
||||
|
||||
.. automodule:: github_sync.management
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
Tests
|
||||
-----
|
||||
|
||||
.. .. automodule:: github_sync.tests
|
||||
.. :members:
|
||||
.. :show-inheritance:
|
||||
@@ -6,4 +6,9 @@ Contents:
|
||||
:maxdepth: 2
|
||||
|
||||
xmodule.rst
|
||||
capa.rst
|
||||
capa.rst
|
||||
chem.rst
|
||||
sandbox-packages.rst
|
||||
symmath.rst
|
||||
calc.rst
|
||||
|
||||
|
||||
@@ -1,32 +1,30 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# MITx documentation build configuration file, created by
|
||||
# sphinx-quickstart on Fri Nov 2 15:43:00 2012.
|
||||
#
|
||||
# This file is execfile()d with the current directory set to its containing dir.
|
||||
#
|
||||
# Note that not all possible configuration values are present in this
|
||||
# autogenerated file.
|
||||
#
|
||||
# All configuration values have a default; values that are commented out
|
||||
# serve to show the default.
|
||||
#pylint: disable=C0103
|
||||
#pylint: disable=W0622
|
||||
#pylint: disable=W0212
|
||||
#pylint: disable=W0613
|
||||
""" EdX documentation build configuration file, created by
|
||||
sphinx-quickstart on Fri Nov 2 15:43:00 2012.
|
||||
|
||||
import sys, os
|
||||
This file is execfile()d with the current directory set to its containing dir.
|
||||
|
||||
Note that not all possible configuration values are present in this
|
||||
autogenerated file.
|
||||
|
||||
All configuration values have a default; values that are commented out
|
||||
serve to show the default."""
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
# If extensions (or modules to document with autodoc) are in another directory,
|
||||
# add these directories to sys.path here. If the directory is relative to the
|
||||
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||
sys.path.insert(0, os.path.abspath('.'))
|
||||
# sys.path.insert(0, os.path.abspath('.'))
|
||||
sys.path.insert(0, os.path.abspath('../..')) # mitx folder
|
||||
sys.path.insert(0, os.path.join(os.path.abspath('../..'), 'common', 'lib', 'capa')) # capa module
|
||||
sys.path.insert(0, os.path.join(os.path.abspath('../..'), 'common', 'lib', 'xmodule')) # xmodule
|
||||
sys.path.insert(0, os.path.join(os.path.abspath('../..'), 'lms', 'djangoapps')) # lms djangoapps
|
||||
sys.path.insert(0, os.path.join(os.path.abspath('../..'), 'cms', 'djangoapps')) # cms djangoapps
|
||||
sys.path.insert(0, os.path.join(os.path.abspath('../..'), 'common', 'djangoapps')) # common djangoapps
|
||||
|
||||
# django configuration - careful here
|
||||
import os
|
||||
os.environ['DJANGO_SETTINGS_MODULE'] = 'lms.envs.dev'
|
||||
os.environ['DJANGO_SETTINGS_MODULE'] = 'lms.envs.test'
|
||||
|
||||
|
||||
# -- General configuration -----------------------------------------------------
|
||||
@@ -36,7 +34,9 @@ os.environ['DJANGO_SETTINGS_MODULE'] = 'lms.envs.dev'
|
||||
|
||||
# Add any Sphinx extension module names here, as strings. They can be extensions
|
||||
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
|
||||
extensions = ['sphinx.ext.autodoc', 'sphinx.ext.doctest', 'sphinx.ext.intersphinx', 'sphinx.ext.todo', 'sphinx.ext.coverage', 'sphinx.ext.pngmath', 'sphinx.ext.mathjax', 'sphinx.ext.viewcode']
|
||||
extensions = [
|
||||
'sphinx.ext.autodoc', 'sphinx.ext.doctest', 'sphinx.ext.intersphinx', 'sphinx.ext.todo', 'sphinx.ext.coverage',
|
||||
'sphinx.ext.pngmath', 'sphinx.ext.mathjax', 'sphinx.ext.viewcode']
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
templates_path = ['_templates']
|
||||
@@ -51,17 +51,17 @@ source_suffix = '.rst'
|
||||
master_doc = 'index'
|
||||
|
||||
# General information about the project.
|
||||
project = u'MITx'
|
||||
copyright = u'2012, MITx team'
|
||||
project = u'EdX Dev Data'
|
||||
copyright = u'2012-13, EdX team'
|
||||
|
||||
# The version info for the project you're documenting, acts as replacement for
|
||||
# |version| and |release|, also used in various other places throughout the
|
||||
# built documents.
|
||||
#
|
||||
# The short X.Y version.
|
||||
version = '1.0'
|
||||
version = '0.2'
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
release = '1.0'
|
||||
release = '0.2'
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
@@ -75,7 +75,7 @@ release = '1.0'
|
||||
|
||||
# List of patterns, relative to source directory, that match files and
|
||||
# directories to ignore when looking for source files.
|
||||
exclude_patterns = []
|
||||
exclude_patterns = ['build']
|
||||
|
||||
# The reST default role (used for this markup: `text`) to use for all documents.
|
||||
#default_role = None
|
||||
@@ -175,27 +175,27 @@ html_static_path = ['_static']
|
||||
#html_file_suffix = None
|
||||
|
||||
# Output file base name for HTML help builder.
|
||||
htmlhelp_basename = 'MITxdoc'
|
||||
htmlhelp_basename = 'edXDocs'
|
||||
|
||||
|
||||
# -- Options for LaTeX output --------------------------------------------------
|
||||
|
||||
latex_elements = {
|
||||
# The paper size ('letterpaper' or 'a4paper').
|
||||
#'papersize': 'letterpaper',
|
||||
# The paper size ('letterpaper' or 'a4paper').
|
||||
#'papersize': 'letterpaper',
|
||||
|
||||
# The font size ('10pt', '11pt' or '12pt').
|
||||
#'pointsize': '10pt',
|
||||
# The font size ('10pt', '11pt' or '12pt').
|
||||
#'pointsize': '10pt',
|
||||
|
||||
# Additional stuff for the LaTeX preamble.
|
||||
#'preamble': '',
|
||||
# Additional stuff for the LaTeX preamble.
|
||||
#'preamble': '',
|
||||
}
|
||||
|
||||
# Grouping the document tree into LaTeX files. List of tuples
|
||||
# (source start file, target name, title, author, documentclass [howto/manual]).
|
||||
latex_documents = [
|
||||
('index', 'MITx.tex', u'MITx Documentation',
|
||||
u'MITx team', 'manual'),
|
||||
('index', 'edXDocs.tex', u'EdX Dev Data Documentation',
|
||||
u'EdX Team', 'manual'),
|
||||
]
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top of
|
||||
@@ -224,8 +224,8 @@ latex_documents = [
|
||||
# One entry per manual page. List of tuples
|
||||
# (source start file, name, description, authors, manual section).
|
||||
man_pages = [
|
||||
('index', 'mitx', u'MITx Documentation',
|
||||
[u'MITx team'], 1)
|
||||
('index', 'edxdocs', u'EdX Dev Data Documentation',
|
||||
[u'EdX Team'], 1)
|
||||
]
|
||||
|
||||
# If true, show URL addresses after external links.
|
||||
@@ -238,9 +238,9 @@ man_pages = [
|
||||
# (source start file, target name, title, author,
|
||||
# dir menu entry, description, category)
|
||||
texinfo_documents = [
|
||||
('index', 'MITx', u'MITx Documentation',
|
||||
u'MITx team', 'MITx', 'One line description of project.',
|
||||
'Miscellaneous'),
|
||||
('index', 'EdXDocs', u'EdX Dev Data Documentation',
|
||||
u'EdX Team', 'EdXDocs', 'One line description of project.',
|
||||
'Miscellaneous'),
|
||||
]
|
||||
|
||||
# Documents to append as an appendix to all manuals.
|
||||
@@ -265,8 +265,12 @@ from django.utils.encoding import force_unicode
|
||||
|
||||
|
||||
def process_docstring(app, what, name, obj, options, lines):
|
||||
"""Autodoc django models"""
|
||||
|
||||
# This causes import errors if left outside the function
|
||||
from django.db import models
|
||||
|
||||
# If you want extract docs from django forms:
|
||||
# from django import forms
|
||||
# from django.forms.models import BaseInlineFormSet
|
||||
|
||||
@@ -326,5 +330,6 @@ def process_docstring(app, what, name, obj, options, lines):
|
||||
|
||||
|
||||
def setup(app):
|
||||
# Register the docstring processor with sphinx
|
||||
"""Setup docsting processors"""
|
||||
#Register the docstring processor with sphinx
|
||||
app.connect('autodoc-process-docstring', process_docstring)
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
.. MITx documentation master file, created by
|
||||
.. EdX Dev documentation master file, created by
|
||||
sphinx-quickstart on Fri Nov 2 15:43:00 2012.
|
||||
You can adapt this file completely to your liking, but it should at least
|
||||
contain the root `toctree` directive.
|
||||
|
||||
Welcome to MITx's documentation!
|
||||
================================
|
||||
Welcome to EdX's Dev documentation!
|
||||
===================================
|
||||
|
||||
Contents:
|
||||
|
||||
|
||||
@@ -1,20 +1,9 @@
|
||||
*******************************************
|
||||
What the pieces are?
|
||||
Overview
|
||||
*******************************************
|
||||
|
||||
What
|
||||
====
|
||||
|
||||
...
|
||||
This is EdX Dev documentation, mainly extracted from docstrings.
|
||||
Autogenerated by Sphinx from python code.
|
||||
Soon support for JS will be impemented.
|
||||
|
||||
How
|
||||
===
|
||||
|
||||
...
|
||||
|
||||
|
||||
Who
|
||||
===
|
||||
|
||||
|
||||
...
|
||||
11
docs/source/sandbox-packages.rst
Normal file
11
docs/source/sandbox-packages.rst
Normal file
@@ -0,0 +1,11 @@
|
||||
*******************************************
|
||||
Sandbox-packages
|
||||
*******************************************
|
||||
.. module:: sandbox-packages
|
||||
|
||||
Loncapa
|
||||
=======
|
||||
|
||||
.. automodule:: loncapa.loncapa_check
|
||||
:members:
|
||||
:show-inheritance:
|
||||
31
docs/source/symmath.rst
Normal file
31
docs/source/symmath.rst
Normal file
@@ -0,0 +1,31 @@
|
||||
*******************************************
|
||||
Symmath
|
||||
*******************************************
|
||||
|
||||
.. module:: symmath
|
||||
|
||||
|
||||
Formula
|
||||
=======
|
||||
|
||||
.. automodule:: symmath.formula
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
Symmath check
|
||||
=============
|
||||
|
||||
.. automodule:: symmath.symmath_check
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
Symmath tests
|
||||
=============
|
||||
|
||||
.. automodule:: symmath.test_formula
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
.. automodule:: symmath.test_symmath_check
|
||||
:members:
|
||||
:show-inheritance:
|
||||
@@ -144,13 +144,6 @@ Templates
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
Time parse
|
||||
==========
|
||||
|
||||
.. automodule:: xmodule.timeparse
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
Vertical
|
||||
========
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ from certificates.models import certificate_status_for_student
|
||||
from certificates.models import CertificateStatuses as status
|
||||
from certificates.models import CertificateWhitelist
|
||||
|
||||
from mitxmako.middleware import MakoMiddleware
|
||||
from courseware import grades, courses
|
||||
from django.test.client import RequestFactory
|
||||
from capa.xqueue_interface import XQueueInterface
|
||||
@@ -51,6 +52,14 @@ class XQueueCertInterface(object):
|
||||
"""
|
||||
|
||||
def __init__(self, request=None):
|
||||
# MakoMiddleware Note:
|
||||
# Line below has the side-effect of writing to a module level lookup
|
||||
# table that will allow problems to render themselves. If this is not
|
||||
# present, problems that a student hasn't seen will error when loading,
|
||||
# causing the grading system to under-count the possible score and
|
||||
# inflate their grade. This dependency is bad and was probably recently
|
||||
# introduced. This is the bandage until we can trace the root cause.
|
||||
m = MakoMiddleware()
|
||||
|
||||
# Get basic auth (username/password) for
|
||||
# xqueue connection if it's in the settings
|
||||
@@ -161,6 +170,10 @@ class XQueueCertInterface(object):
|
||||
cert, created = GeneratedCertificate.objects.get_or_create(
|
||||
user=student, course_id=course_id)
|
||||
|
||||
# Needed
|
||||
self.request.user = student
|
||||
self.request.session = {}
|
||||
|
||||
grade = grades.grade(student, self.request, course)
|
||||
is_whitelisted = self.whitelist.filter(
|
||||
user=student, course_id=course_id, whitelist=True).exists()
|
||||
@@ -211,5 +224,5 @@ class XQueueCertInterface(object):
|
||||
(error, msg) = self.xqueue_interface.send_to_queue(
|
||||
header=xheader, body=json.dumps(contents))
|
||||
if error:
|
||||
logger.critical('Unable to add a request to the queue')
|
||||
logger.critical('Unable to add a request to the queue: {} {}'.format(error, msg))
|
||||
raise Exception('Unable to send queue message')
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#pylint: disable=C0111
|
||||
#pylint: disable=W0621
|
||||
# pylint: disable=C0111
|
||||
# pylint: disable=W0621
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
|
||||
@@ -91,7 +91,7 @@ def click_on_section(step, section):
|
||||
@step(u'I click on subsection "([^"]*)"$')
|
||||
def click_on_subsection(step, subsection):
|
||||
subsection_css = 'ul[id="ui-accordion-accordion-panel-0"]> li > a'
|
||||
world.css_find(subsection_css)[int(subsection) - 1].click()
|
||||
world.css_click(subsection_css, index=(int(subsection) - 1))
|
||||
|
||||
|
||||
@step(u'I click on sequence "([^"]*)"$')
|
||||
|
||||
@@ -112,12 +112,12 @@ def assert_problem_has_answer(step, problem_type, answer_class):
|
||||
|
||||
|
||||
@step(u'I reset the problem')
|
||||
def reset_problem(step):
|
||||
def reset_problem(_step):
|
||||
world.css_click('input.reset')
|
||||
|
||||
|
||||
@step(u'I press the button with the label "([^"]*)"$')
|
||||
def press_the_button_with_label(step, buttonname):
|
||||
def press_the_button_with_label(_step, buttonname):
|
||||
button_css = 'button span.show-label'
|
||||
elem = world.css_find(button_css).first
|
||||
assert_equal(elem.text, buttonname)
|
||||
@@ -125,7 +125,7 @@ def press_the_button_with_label(step, buttonname):
|
||||
|
||||
|
||||
@step(u'The "([^"]*)" button does( not)? appear')
|
||||
def action_button_present(step, buttonname, doesnt_appear):
|
||||
def action_button_present(_step, buttonname, doesnt_appear):
|
||||
button_css = 'section.action input[value*="%s"]' % buttonname
|
||||
if doesnt_appear:
|
||||
assert world.is_css_not_present(button_css)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#pylint: disable=C0111
|
||||
#pylint: disable=W0621
|
||||
# pylint: disable=C0111
|
||||
# pylint: disable=W0621
|
||||
|
||||
from lettuce import world, step
|
||||
from lettuce.django import django_url
|
||||
@@ -7,7 +7,7 @@ from common import TEST_COURSE_ORG, TEST_COURSE_NAME
|
||||
|
||||
|
||||
@step('I register for the course "([^"]*)"$')
|
||||
def i_register_for_the_course(step, course):
|
||||
def i_register_for_the_course(_step, course):
|
||||
cleaned_name = TEST_COURSE_NAME.replace(' ', '_')
|
||||
url = django_url('courses/%s/%s/%s/about' % (TEST_COURSE_ORG, course, cleaned_name))
|
||||
world.browser.visit(url)
|
||||
@@ -20,13 +20,13 @@ def i_register_for_the_course(step, course):
|
||||
|
||||
|
||||
@step(u'I should see an empty dashboard message')
|
||||
def i_should_see_empty_dashboard(step):
|
||||
def i_should_see_empty_dashboard(_step):
|
||||
empty_dash_css = 'section.empty-dashboard-message'
|
||||
assert world.is_css_present(empty_dash_css)
|
||||
|
||||
|
||||
@step(u'I should( NOT)? see the course numbered "([^"]*)" in my dashboard$')
|
||||
def i_should_see_that_course_in_my_dashboard(step, doesnt_appear, course):
|
||||
def i_should_see_that_course_in_my_dashboard(_step, doesnt_appear, course):
|
||||
course_link_css = 'section.my-courses a[href*="%s"]' % course
|
||||
if doesnt_appear:
|
||||
assert world.is_css_not_present(course_link_css)
|
||||
@@ -35,7 +35,7 @@ def i_should_see_that_course_in_my_dashboard(step, doesnt_appear, course):
|
||||
|
||||
|
||||
@step(u'I unregister for the course numbered "([^"]*)"')
|
||||
def i_unregister_for_that_course(step, course):
|
||||
def i_unregister_for_that_course(_step, course):
|
||||
unregister_css = 'section.info a[href*="#unenroll-modal"][data-course-number*="%s"]' % course
|
||||
world.css_click(unregister_css)
|
||||
button_css = 'section#unenroll-modal input[value="Unregister"]'
|
||||
|
||||
@@ -8,12 +8,12 @@ from common import TEST_COURSE_NAME, TEST_SECTION_NAME, i_am_registered_for_the_
|
||||
|
||||
|
||||
@step('when I view the video it has autoplay enabled')
|
||||
def does_autoplay(step):
|
||||
def does_autoplay(_step):
|
||||
assert(world.css_find('.video')[0]['data-autoplay'] == 'True')
|
||||
|
||||
|
||||
@step('the course has a Video component')
|
||||
def view_video(step):
|
||||
def view_video(_step):
|
||||
coursename = TEST_COURSE_NAME.replace(' ', '_')
|
||||
i_am_registered_for_the_course(step, coursename)
|
||||
|
||||
|
||||
@@ -4,9 +4,9 @@ WE'RE USING MIGRATIONS!
|
||||
If you make changes to this model, be sure to create an appropriate migration
|
||||
file and check it in at the same time as your model changes. To do that,
|
||||
|
||||
1. Go to the mitx dir
|
||||
1. Go to the edx-platform dir
|
||||
2. ./manage.py schemamigration courseware --auto description_of_your_change
|
||||
3. Add the migration file created in mitx/courseware/migrations/
|
||||
3. Add the migration file created in edx-platform/lms/djangoapps/courseware/migrations/
|
||||
|
||||
|
||||
ASSUMPTIONS: modules have unique IDs, even across different module_types
|
||||
@@ -17,6 +17,7 @@ from django.db import models
|
||||
from django.db.models.signals import post_save
|
||||
from django.dispatch import receiver
|
||||
|
||||
|
||||
class StudentModule(models.Model):
|
||||
"""
|
||||
Keeps student state for a particular module in a particular course.
|
||||
|
||||
@@ -121,7 +121,7 @@ def toc_for_course(user, request, course, active_chapter, active_section, model_
|
||||
|
||||
|
||||
def get_module(user, request, location, model_data_cache, course_id,
|
||||
position=None, not_found_ok = False, wrap_xmodule_display=True,
|
||||
position=None, not_found_ok=False, wrap_xmodule_display=True,
|
||||
grade_bucket_type=None, depth=0):
|
||||
"""
|
||||
Get an instance of the xmodule class identified by location,
|
||||
@@ -161,16 +161,49 @@ def get_module(user, request, location, model_data_cache, course_id,
|
||||
return None
|
||||
|
||||
|
||||
def get_module_for_descriptor(user, request, descriptor, model_data_cache, course_id,
|
||||
position=None, wrap_xmodule_display=True, grade_bucket_type=None):
|
||||
"""
|
||||
Actually implement get_module. See docstring there for details.
|
||||
def get_xqueue_callback_url_prefix(request):
|
||||
"""
|
||||
Calculates default prefix based on request, but allows override via settings
|
||||
|
||||
This is separated from get_module_for_descriptor so that it can be called
|
||||
by the LMS before submitting background tasks to run. The xqueue callbacks
|
||||
should go back to the LMS, not to the worker.
|
||||
"""
|
||||
prefix = '{proto}://{host}'.format(
|
||||
proto=request.META.get('HTTP_X_FORWARDED_PROTO', 'https' if request.is_secure() else 'http'),
|
||||
host=request.get_host()
|
||||
)
|
||||
return settings.XQUEUE_INTERFACE.get('callback_url', prefix)
|
||||
|
||||
|
||||
def get_module_for_descriptor(user, request, descriptor, model_data_cache, course_id,
|
||||
position=None, wrap_xmodule_display=True, grade_bucket_type=None):
|
||||
"""
|
||||
Implements get_module, extracting out the request-specific functionality.
|
||||
|
||||
See get_module() docstring for further details.
|
||||
"""
|
||||
# allow course staff to masquerade as student
|
||||
if has_access(user, descriptor, 'staff', course_id):
|
||||
setup_masquerade(request, True)
|
||||
|
||||
track_function = make_track_function(request)
|
||||
xqueue_callback_url_prefix = get_xqueue_callback_url_prefix(request)
|
||||
|
||||
return get_module_for_descriptor_internal(user, descriptor, model_data_cache, course_id,
|
||||
track_function, xqueue_callback_url_prefix,
|
||||
position, wrap_xmodule_display, grade_bucket_type)
|
||||
|
||||
|
||||
def get_module_for_descriptor_internal(user, descriptor, model_data_cache, course_id,
|
||||
track_function, xqueue_callback_url_prefix,
|
||||
position=None, wrap_xmodule_display=True, grade_bucket_type=None):
|
||||
"""
|
||||
Actually implement get_module, without requiring a request.
|
||||
|
||||
See get_module() docstring for further details.
|
||||
"""
|
||||
|
||||
# Short circuit--if the user shouldn't have access, bail without doing any work
|
||||
if not has_access(user, descriptor, 'load', course_id):
|
||||
return None
|
||||
@@ -186,19 +219,13 @@ def get_module_for_descriptor(user, request, descriptor, model_data_cache, cours
|
||||
|
||||
def make_xqueue_callback(dispatch='score_update'):
|
||||
# Fully qualified callback URL for external queueing system
|
||||
xqueue_callback_url = '{proto}://{host}'.format(
|
||||
host=request.get_host(),
|
||||
proto=request.META.get('HTTP_X_FORWARDED_PROTO', 'https' if request.is_secure() else 'http')
|
||||
)
|
||||
xqueue_callback_url = settings.XQUEUE_INTERFACE.get('callback_url',xqueue_callback_url) # allow override
|
||||
|
||||
xqueue_callback_url += reverse('xqueue_callback',
|
||||
kwargs=dict(course_id=course_id,
|
||||
userid=str(user.id),
|
||||
id=descriptor.location.url(),
|
||||
dispatch=dispatch),
|
||||
)
|
||||
return xqueue_callback_url
|
||||
relative_xqueue_callback_url = reverse('xqueue_callback',
|
||||
kwargs=dict(course_id=course_id,
|
||||
userid=str(user.id),
|
||||
id=descriptor.location.url(),
|
||||
dispatch=dispatch),
|
||||
)
|
||||
return xqueue_callback_url_prefix + relative_xqueue_callback_url
|
||||
|
||||
# Default queuename is course-specific and is derived from the course that
|
||||
# contains the current module.
|
||||
@@ -211,20 +238,20 @@ def get_module_for_descriptor(user, request, descriptor, model_data_cache, cours
|
||||
'waittime': settings.XQUEUE_WAITTIME_BETWEEN_REQUESTS
|
||||
}
|
||||
|
||||
#This is a hacky way to pass settings to the combined open ended xmodule
|
||||
#It needs an S3 interface to upload images to S3
|
||||
#It needs the open ended grading interface in order to get peer grading to be done
|
||||
#this first checks to see if the descriptor is the correct one, and only sends settings if it is
|
||||
# This is a hacky way to pass settings to the combined open ended xmodule
|
||||
# It needs an S3 interface to upload images to S3
|
||||
# It needs the open ended grading interface in order to get peer grading to be done
|
||||
# this first checks to see if the descriptor is the correct one, and only sends settings if it is
|
||||
|
||||
#Get descriptor metadata fields indicating needs for various settings
|
||||
# Get descriptor metadata fields indicating needs for various settings
|
||||
needs_open_ended_interface = getattr(descriptor, "needs_open_ended_interface", False)
|
||||
needs_s3_interface = getattr(descriptor, "needs_s3_interface", False)
|
||||
|
||||
#Initialize interfaces to None
|
||||
# Initialize interfaces to None
|
||||
open_ended_grading_interface = None
|
||||
s3_interface = None
|
||||
|
||||
#Create interfaces if needed
|
||||
# Create interfaces if needed
|
||||
if needs_open_ended_interface:
|
||||
open_ended_grading_interface = settings.OPEN_ENDED_GRADING_INTERFACE
|
||||
open_ended_grading_interface['mock_peer_grading'] = settings.MOCK_PEER_GRADING
|
||||
@@ -238,10 +265,15 @@ def get_module_for_descriptor(user, request, descriptor, model_data_cache, cours
|
||||
|
||||
def inner_get_module(descriptor):
|
||||
"""
|
||||
Delegate to get_module. It does an access check, so may return None
|
||||
Delegate to get_module_for_descriptor_internal() with all values except `descriptor` set.
|
||||
|
||||
Because it does an access check, it may return None.
|
||||
"""
|
||||
return get_module_for_descriptor(user, request, descriptor,
|
||||
model_data_cache, course_id, position)
|
||||
# TODO: fix this so that make_xqueue_callback uses the descriptor passed into
|
||||
# inner_get_module, not the parent's callback. Add it as an argument....
|
||||
return get_module_for_descriptor_internal(user, descriptor, model_data_cache, course_id,
|
||||
track_function, make_xqueue_callback,
|
||||
position, wrap_xmodule_display, grade_bucket_type)
|
||||
|
||||
def xblock_model_data(descriptor):
|
||||
return DbModel(
|
||||
@@ -266,7 +298,7 @@ def get_module_for_descriptor(user, request, descriptor, model_data_cache, cours
|
||||
student_module.max_grade = event.get('max_value')
|
||||
student_module.save()
|
||||
|
||||
#Bin score into range and increment stats
|
||||
# Bin score into range and increment stats
|
||||
score_bucket = get_score_bucket(student_module.grade, student_module.max_grade)
|
||||
org, course_num, run = course_id.split("/")
|
||||
|
||||
@@ -291,7 +323,7 @@ def get_module_for_descriptor(user, request, descriptor, model_data_cache, cours
|
||||
# TODO (cpennington): When modules are shared between courses, the static
|
||||
# prefix is going to have to be specific to the module, not the directory
|
||||
# that the xml was loaded from
|
||||
system = ModuleSystem(track_function=make_track_function(request),
|
||||
system = ModuleSystem(track_function=track_function,
|
||||
render_template=render_to_string,
|
||||
ajax_url=ajax_url,
|
||||
xqueue=xqueue,
|
||||
@@ -440,13 +472,13 @@ def modx_dispatch(request, dispatch, location, course_id):
|
||||
inputfiles = request.FILES.getlist(fileinput_id)
|
||||
|
||||
if len(inputfiles) > settings.MAX_FILEUPLOADS_PER_INPUT:
|
||||
too_many_files_msg = 'Submission aborted! Maximum %d files may be submitted at once' %\
|
||||
too_many_files_msg = 'Submission aborted! Maximum %d files may be submitted at once' % \
|
||||
settings.MAX_FILEUPLOADS_PER_INPUT
|
||||
return HttpResponse(json.dumps({'success': too_many_files_msg}))
|
||||
|
||||
for inputfile in inputfiles:
|
||||
if inputfile.size > settings.STUDENT_FILEUPLOAD_MAX_SIZE: # Bytes
|
||||
file_too_big_msg = 'Submission aborted! Your file "%s" is too large (max size: %d MB)' %\
|
||||
if inputfile.size > settings.STUDENT_FILEUPLOAD_MAX_SIZE: # Bytes
|
||||
file_too_big_msg = 'Submission aborted! Your file "%s" is too large (max size: %d MB)' % \
|
||||
(inputfile.name, settings.STUDENT_FILEUPLOAD_MAX_SIZE / (1000 ** 2))
|
||||
return HttpResponse(json.dumps({'success': file_too_big_msg}))
|
||||
p[fileinput_id] = inputfiles
|
||||
|
||||
@@ -13,7 +13,7 @@ from django.test.client import Client
|
||||
|
||||
from student.tests.factories import UserFactory, CourseEnrollmentFactory
|
||||
from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE
|
||||
from xmodule.tests import test_system
|
||||
from xmodule.tests import get_test_system
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
|
||||
@@ -77,7 +77,7 @@ class BaseTestXmodule(ModuleStoreTestCase):
|
||||
data=self.DATA
|
||||
)
|
||||
|
||||
system = test_system()
|
||||
system = get_test_system()
|
||||
system.render_template = lambda template, context: context
|
||||
model_data = {'location': self.item_descriptor.location}
|
||||
model_data.update(self.MODEL_DATA)
|
||||
|
||||
@@ -11,21 +11,22 @@ from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
|
||||
|
||||
class ProgressTestCase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
def setUp(self):
|
||||
|
||||
self.mockuser1 = MagicMock()
|
||||
self.mockuser0 = MagicMock()
|
||||
self.course = MagicMock()
|
||||
self.mockuser1.is_authenticated.return_value = True
|
||||
self.mockuser0.is_authenticated.return_value = False
|
||||
self.course.id = 'edX/full/6.002_Spring_2012'
|
||||
self.tab = {'name': 'same'}
|
||||
self.active_page1 = 'progress'
|
||||
self.active_page0 = 'stagnation'
|
||||
self.mockuser1 = MagicMock()
|
||||
self.mockuser0 = MagicMock()
|
||||
self.course = MagicMock()
|
||||
self.mockuser1.is_authenticated.return_value = True
|
||||
self.mockuser0.is_authenticated.return_value = False
|
||||
self.course.id = 'edX/full/6.002_Spring_2012'
|
||||
self.tab = {'name': 'same'}
|
||||
self.active_page1 = 'progress'
|
||||
self.active_page0 = 'stagnation'
|
||||
|
||||
def test_progress(self):
|
||||
def test_progress(self):
|
||||
|
||||
self.assertEqual(tabs._progress(self.tab, self.mockuser0, self.course,
|
||||
self.active_page0), [])
|
||||
@@ -34,8 +35,8 @@ class ProgressTestCase(TestCase):
|
||||
self.active_page1)[0].name, 'same')
|
||||
|
||||
self.assertEqual(tabs._progress(self.tab, self.mockuser1, self.course,
|
||||
self.active_page1)[0].link,
|
||||
reverse('progress', args = [self.course.id]))
|
||||
self.active_page1)[0].link,
|
||||
reverse('progress', args=[self.course.id]))
|
||||
|
||||
self.assertEqual(tabs._progress(self.tab, self.mockuser1, self.course,
|
||||
self.active_page0)[0].is_active, False)
|
||||
@@ -63,15 +64,15 @@ class WikiTestCase(TestCase):
|
||||
'same')
|
||||
|
||||
self.assertEqual(tabs._wiki(self.tab, self.user,
|
||||
self.course, self.active_page1)[0].link,
|
||||
self.course, self.active_page1)[0].link,
|
||||
reverse('course_wiki', args=[self.course.id]))
|
||||
|
||||
self.assertEqual(tabs._wiki(self.tab, self.user,
|
||||
self.course, self.active_page1)[0].is_active,
|
||||
self.course, self.active_page1)[0].is_active,
|
||||
True)
|
||||
|
||||
self.assertEqual(tabs._wiki(self.tab, self.user,
|
||||
self.course, self.active_page0)[0].is_active,
|
||||
self.course, self.active_page0)[0].is_active,
|
||||
False)
|
||||
|
||||
@override_settings(WIKI_ENABLED=False)
|
||||
@@ -129,14 +130,13 @@ class StaticTabTestCase(TestCase):
|
||||
|
||||
self.assertEqual(tabs._static_tab(self.tabby, self.user,
|
||||
self.course, self.active_page1)[0].link,
|
||||
reverse('static_tab', args = [self.course.id,
|
||||
self.tabby['url_slug']]))
|
||||
reverse('static_tab', args=[self.course.id,
|
||||
self.tabby['url_slug']]))
|
||||
|
||||
self.assertEqual(tabs._static_tab(self.tabby, self.user,
|
||||
self.course, self.active_page1)[0].is_active,
|
||||
True)
|
||||
|
||||
|
||||
self.assertEqual(tabs._static_tab(self.tabby, self.user,
|
||||
self.course, self.active_page0)[0].is_active,
|
||||
False)
|
||||
@@ -183,7 +183,7 @@ class TextbooksTestCase(TestCase):
|
||||
|
||||
self.assertEqual(tabs._textbooks(self.tab, self.mockuser1,
|
||||
self.course, self.active_page1)[1].name,
|
||||
'Topology')
|
||||
'Topology')
|
||||
|
||||
self.assertEqual(tabs._textbooks(self.tab, self.mockuser1,
|
||||
self.course, self.active_page1)[1].link,
|
||||
@@ -206,6 +206,7 @@ class TextbooksTestCase(TestCase):
|
||||
self.assertEqual(tabs._textbooks(self.tab, self.mockuser0,
|
||||
self.course, self.active_pageX), [])
|
||||
|
||||
|
||||
class KeyCheckerTestCase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
@@ -223,39 +224,36 @@ class KeyCheckerTestCase(TestCase):
|
||||
|
||||
class NullValidatorTestCase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
def setUp(self):
|
||||
|
||||
self.d = {}
|
||||
self.dummy = {}
|
||||
|
||||
def test_null_validator(self):
|
||||
|
||||
self.assertIsNone(tabs.null_validator(self.d))
|
||||
def test_null_validator(self):
|
||||
self.assertIsNone(tabs.null_validator(self.dummy))
|
||||
|
||||
|
||||
class ValidateTabsTestCase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
self.courses = [MagicMock() for i in range(0,5)]
|
||||
self.courses = [MagicMock() for i in range(0, 5)]
|
||||
|
||||
self.courses[0].tabs = None
|
||||
|
||||
self.courses[1].tabs = [{'type':'courseware'}, {'type': 'fax'}]
|
||||
self.courses[1].tabs = [{'type': 'courseware'}, {'type': 'fax'}]
|
||||
|
||||
self.courses[2].tabs = [{'type':'shadow'}, {'type': 'course_info'}]
|
||||
self.courses[2].tabs = [{'type': 'shadow'}, {'type': 'course_info'}]
|
||||
|
||||
self.courses[3].tabs = [{'type':'courseware'},{'type':'course_info', 'name': 'alice'},
|
||||
{'type': 'wiki', 'name':'alice'}, {'type':'discussion', 'name': 'alice'},
|
||||
{'type':'external_link', 'name': 'alice', 'link':'blink'},
|
||||
{'type':'textbooks'}, {'type':'progress', 'name': 'alice'},
|
||||
{'type':'static_tab', 'name':'alice', 'url_slug':'schlug'},
|
||||
{'type': 'staff_grading'}]
|
||||
|
||||
self.courses[4].tabs = [{'type':'courseware'},{'type': 'course_info'}, {'type': 'flying'}]
|
||||
self.courses[3].tabs = [{'type': 'courseware'}, {'type': 'course_info', 'name': 'alice'},
|
||||
{'type': 'wiki', 'name': 'alice'}, {'type': 'discussion', 'name': 'alice'},
|
||||
{'type': 'external_link', 'name': 'alice', 'link': 'blink'},
|
||||
{'type': 'textbooks'}, {'type': 'progress', 'name': 'alice'},
|
||||
{'type': 'static_tab', 'name': 'alice', 'url_slug': 'schlug'},
|
||||
{'type': 'staff_grading'}]
|
||||
|
||||
self.courses[4].tabs = [{'type': 'courseware'}, {'type': 'course_info'}, {'type': 'flying'}]
|
||||
|
||||
def test_validate_tabs(self):
|
||||
|
||||
self.assertIsNone(tabs.validate_tabs(self.courses[0]))
|
||||
self.assertRaises(tabs.InvalidTabsException, tabs.validate_tabs, self.courses[1])
|
||||
self.assertRaises(tabs.InvalidTabsException, tabs.validate_tabs, self.courses[2])
|
||||
@@ -268,15 +266,15 @@ class DiscussionLinkTestCase(ModuleStoreTestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.tabs_with_discussion = [
|
||||
{'type':'courseware'},
|
||||
{'type':'course_info'},
|
||||
{'type':'discussion'},
|
||||
{'type':'textbooks'},
|
||||
{'type': 'courseware'},
|
||||
{'type': 'course_info'},
|
||||
{'type': 'discussion'},
|
||||
{'type': 'textbooks'},
|
||||
]
|
||||
self.tabs_without_discussion = [
|
||||
{'type':'courseware'},
|
||||
{'type':'course_info'},
|
||||
{'type':'textbooks'},
|
||||
{'type': 'courseware'},
|
||||
{'type': 'course_info'},
|
||||
{'type': 'textbooks'},
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -22,7 +22,7 @@ from django.conf import settings
|
||||
|
||||
from xmodule.videoalpha_module import VideoAlphaDescriptor, VideoAlphaModule
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.tests import test_system
|
||||
from xmodule.tests import get_test_system
|
||||
from xmodule.tests.test_logic import LogicTest
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ class VideoAlphaFactory(object):
|
||||
|
||||
descriptor = Mock(weight="1")
|
||||
|
||||
system = test_system()
|
||||
system = get_test_system()
|
||||
system.render_template = lambda template, context: context
|
||||
VideoAlphaModule.location = location
|
||||
module = VideoAlphaModule(system, descriptor, model_data)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user