Merge branch 'master' into christina/course-settings-drupal

This commit is contained in:
cahrens
2013-06-20 11:00:12 -04:00
136 changed files with 10125 additions and 1033 deletions

View File

@@ -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>

View File

@@ -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`

View File

@@ -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'

View File

@@ -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():

View File

@@ -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$')

View File

@@ -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

View File

@@ -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)

View File

@@ -1,5 +1,5 @@
#pylint: disable=C0111
#pylint: disable=W0621
# pylint: disable=C0111
# pylint: disable=W0621
from lettuce import world, step
from common import *

View File

@@ -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'))

View File

@@ -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

View File

@@ -99,6 +99,7 @@ class ChecklistTestCase(CourseTestCase):
'name': self.course.location.name,
'checklist_index': 2})
def get_first_item(checklist):
return checklist['items'][0]

View File

@@ -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]))

View File

@@ -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

View File

@@ -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',

View File

@@ -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)

View File

@@ -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()

View File

@@ -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'],

View File

@@ -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",

View File

@@ -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")

View File

@@ -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)

View File

@@ -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

View File

@@ -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"
})
});

View File

@@ -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&hellip;")
});
}
if(!this.msgView) {
this.msgView = new CMS.Views.Notification({
model: this.msg,
this.msg = new CMS.Views.Notification.Saving({
title: gettext("Saving&hellip;"),
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();
}
});

View File

@@ -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
});
}
}

View File

@@ -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;
});
});

View File

@@ -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();
}
});

View File

@@ -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>

View File

@@ -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 -->

View File

@@ -1,5 +1,5 @@
#pylint: disable=C0111
#pylint: disable=W0621
# pylint: disable=C0111
# pylint: disable=W0621
from lettuce import world, step
from .factories import *

View File

@@ -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.

View File

@@ -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

View File

@@ -1,4 +1,3 @@
import re
import json
import logging
import static_replace

View File

@@ -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']

View File

@@ -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")

View File

@@ -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

View File

@@ -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']

View File

@@ -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()

View File

@@ -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)

View File

@@ -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
}

View File

@@ -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):
"""

View File

@@ -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}" \

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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', [])

View File

@@ -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')

View File

@@ -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))

View File

@@ -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))

View File

@@ -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'])

View File

@@ -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():

View File

@@ -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

View File

@@ -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).

View File

@@ -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>')

View File

@@ -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

View File

@@ -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)

View File

@@ -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]))

View File

@@ -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(

View File

@@ -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)

View File

@@ -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

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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])

View File

@@ -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)

View File

@@ -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)

View File

@@ -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):

View File

@@ -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

View File

@@ -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'

View File

@@ -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
View 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 '&nbsp;'),
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");
}})();

File diff suppressed because one or more lines are too long

View File

@@ -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 "";

View File

@@ -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.

View File

@@ -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

View File

@@ -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

View File

@@ -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>
...

View File

@@ -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.

View File

@@ -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
View File

@@ -0,0 +1,7 @@
*******************************************
Calc
*******************************************
.. automodule:: calc
:members:
:show-inheritance:

View File

@@ -8,14 +8,6 @@ Contents:
.. toctree::
:maxdepth: 2
chem.rst
Calc
====
.. automodule:: capa.calc
:members:
:show-inheritance:
Capa_problem
============

View File

@@ -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:

View File

@@ -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:

View File

@@ -6,4 +6,9 @@ Contents:
:maxdepth: 2
xmodule.rst
capa.rst
capa.rst
chem.rst
sandbox-packages.rst
symmath.rst
calc.rst

View File

@@ -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)

View File

@@ -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:

View File

@@ -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
===
...

View 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
View 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:

View File

@@ -144,13 +144,6 @@ Templates
:members:
:show-inheritance:
Time parse
==========
.. automodule:: xmodule.timeparse
:members:
:show-inheritance:
Vertical
========

View File

@@ -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')

View File

@@ -1,5 +1,5 @@
#pylint: disable=C0111
#pylint: disable=W0621
# pylint: disable=C0111
# pylint: disable=W0621
from __future__ import absolute_import

View File

@@ -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 "([^"]*)"$')

View File

@@ -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)

View File

@@ -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"]'

View File

@@ -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)

View File

@@ -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.

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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