client app: canvas_quiz_statistics
This commit brings in canvas_quiz_statistics, a client app that can be embedded in Canvas. Closes CNVS-14781, CNVS-14846, CNVS-14847, CNVS-14850 > What's done - full build integration, meaning the app runs with ?optimized_js=1: + JavaScript "built" source gets piped into the r.js build pipeline like any other Canvas JS source in public/javascripts/* + SCSS sources get picked up by the sass-compiler like Canvas style sources and they get compiled from the grounds-up + I18n: phrases are extracted properly, with default values and options, by our i18n rake tasks - new rake task js:build_client_apps that builds client apps and injects them as input to the rest of the JS build process - support for using Canvas JS packages, like d3, jQuery, and Backbone - support for using Canvas SASS variables & helpers - super i18n support: use raw I18n.t() calls like you are in Canvas, with development-time interpolation, as well as super new Handlebars-like block-style translations in React, perfect for very long phrases (mini-articles) > Docs and References The code was originally developed in its own github repository. While I won't be pushing code to that repo anymore, the Wiki will still house the docs until we find a better place. - Repo: https://github.com/amireh/canvas_quiz_statistics - Development guide: http://bit.ly/1sNOhER - Integration guide: http://bit.ly/1m9kA9V > TESTING - login as a teacher - go to /courses/:course_id/quizzes/:quiz_id/statistics_cqs + make sure you see something that looks like the Ember stats + click one of those little "?" help icons, you get a dialog: - verify the contents within the dialog are actual English text, not code gibberish - there's also a link at the end of that dialog, click it and verify it takes you to an Instructure help article - build the assets: `bundle exec rake canvas:compile_assets` then: + add ?optimized_js=1 to the URL and reload the page: - verify the app still works Change-Id: Ic474650dfb06a1c22869ed9680dd04d1ca0f651d Reviewed-on: https://gerrit.instructure.com/39105 Tested-by: Jenkins <jenkins@instructure.com> Reviewed-by: Hannah Bottalla <hannah@instructure.com> Reviewed-by: Adam Ard <aard@instructure.com> QA-Review: Trevor deHaan <tdehaan@instructure.com> Product-Review: Ahmad Amireh <ahmad@instructure.com>
This commit is contained in:
parent
e101bb9fef
commit
80d627eb7a
|
@ -53,6 +53,9 @@ Gemfile.lock3
|
|||
/spec/coffeescripts/plugins/
|
||||
/spec/plugins/
|
||||
|
||||
# generate client app stuff
|
||||
/public/javascripts/client_apps/
|
||||
|
||||
#remove this once we move jqeury into bower
|
||||
/public/javascripts/bower/jquery/
|
||||
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
require [ 'quiz_statistics_cqs' ]
|
|
@ -30,7 +30,7 @@ class Quizzes::QuizzesController < ApplicationController
|
|||
before_filter :require_context
|
||||
add_crumb(proc { t('#crumbs.quizzes', "Quizzes") }) { |c| c.send :named_context_url, c.instance_variable_get("@context"), :context_quizzes_url }
|
||||
before_filter { |c| c.active_tab = "quizzes" }
|
||||
before_filter :require_quiz, :only => [:statistics, :edit, :show, :history, :update, :destroy, :moderate, :read_only, :managed_quiz_data, :submission_versions, :submission_html]
|
||||
before_filter :require_quiz, :only => [:statistics, :statistics_cqs, :edit, :show, :history, :update, :destroy, :moderate, :read_only, :managed_quiz_data, :submission_versions, :submission_html]
|
||||
before_filter :set_download_submission_dialog_title , only: [:show,:statistics]
|
||||
after_filter :lock_results, only: [ :show, :submission_html ]
|
||||
# The number of questions that can display "details". After this number, the "Show details" option is disabled
|
||||
|
@ -468,6 +468,23 @@ class Quizzes::QuizzesController < ApplicationController
|
|||
end
|
||||
end
|
||||
|
||||
def statistics_cqs
|
||||
if authorized_action(@quiz, @current_user, :read_statistics)
|
||||
respond_to do |format|
|
||||
format.html {
|
||||
add_crumb(@quiz.title, named_context_url(@context, :context_quiz_url, @quiz))
|
||||
add_crumb(t(:statistics_cqs_crumb, "Statistics CQS"), named_context_url(@context, :context_quiz_statistics_cqs_url, @quiz))
|
||||
|
||||
js_env({
|
||||
quiz_url: api_v1_course_quiz_url(@context, @quiz),
|
||||
quiz_statistics_url: api_v1_course_quiz_statistics_url(@context, @quiz),
|
||||
quiz_reports_url: api_v1_course_quiz_reports_url(@context, @quiz),
|
||||
})
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def managed_quiz_data
|
||||
extend Api::V1::User
|
||||
if authorized_action(@quiz, @current_user, [:grade, :read_statistics])
|
||||
|
|
|
@ -33,6 +33,11 @@ module Quizzes
|
|||
# PS: this is always true for item analysis
|
||||
:includes_all_versions,
|
||||
|
||||
:points_possible,
|
||||
|
||||
:speed_grader_url,
|
||||
:quiz_submissions_zip_url,
|
||||
|
||||
# an aggregate of question stats from both student and item analysis
|
||||
:question_statistics,
|
||||
|
||||
|
@ -47,13 +52,15 @@ module Quizzes
|
|||
# - score_low
|
||||
# - score_stdev
|
||||
# - user_ids (id set)
|
||||
:submission_statistics
|
||||
:submission_statistics,
|
||||
]
|
||||
|
||||
def_delegators :@controller,
|
||||
:course_quiz_statistics_url,
|
||||
:api_v1_course_quiz_url,
|
||||
:api_v1_course_quiz_statistics_url
|
||||
:api_v1_course_quiz_statistics_url,
|
||||
:speed_grader_course_gradebook_url,
|
||||
:course_quiz_quiz_submissions_url
|
||||
|
||||
has_one :quiz, embed: :ids
|
||||
|
||||
|
@ -118,8 +125,28 @@ module Quizzes
|
|||
object[:student_analysis].includes_all_versions
|
||||
end
|
||||
|
||||
def points_possible
|
||||
quiz.points_possible
|
||||
end
|
||||
|
||||
def speed_grader_url
|
||||
if show_speed_grader?
|
||||
speed_grader_course_gradebook_url(quiz.context, {
|
||||
assignment_id: quiz.assignment.id
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
def quiz_submissions_zip_url
|
||||
course_quiz_quiz_submissions_url(quiz.context, quiz.id, zip: 1)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def show_speed_grader?
|
||||
quiz.assignment.present? && quiz.published? && context.allows_speed_grader?
|
||||
end
|
||||
|
||||
def student_analysis_report
|
||||
@student_analysis_report ||= object[:student_analysis].report.generate(false)
|
||||
end
|
||||
|
@ -127,5 +154,9 @@ module Quizzes
|
|||
def item_analysis_report
|
||||
@item_analysis_report ||= object[:item_analysis].report.generate(false)
|
||||
end
|
||||
|
||||
def quiz
|
||||
object.quiz
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1 @@
|
|||
@import "../../../client_apps/canvas_quiz_statistics/src/css/app";
|
|
@ -9,6 +9,7 @@
|
|||
paths: <%= raw Canvas::RequireJs.paths(true) %>,
|
||||
packages : <%= raw Canvas::RequireJs.packages %>,
|
||||
shim: <%= raw Canvas::RequireJs.shims %>,
|
||||
map: <%= raw Canvas::RequireJs.map %>,
|
||||
waitSeconds: 60
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
<% content_for :page_title, join_title(@quiz.title, t(:page_title, "Statistics CQS")) %>
|
||||
<% jammit_css :quizzes, :canvas_quiz_statistics %>
|
||||
<% js_bundle :quiz_statistics_cqs %>
|
|
@ -0,0 +1,5 @@
|
|||
# --------------------------------------------------------------------------- #
|
||||
# TOTALLY EXPERIMENTAL. DO NOT TOUCH. TOTALLY EXPERIMENTAL.
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
Yes.
|
|
@ -0,0 +1,30 @@
|
|||
[autogen]
|
||||
/.sublime-project.sublime-workspace
|
||||
/.jshint.html
|
||||
/doc/api
|
||||
/doc/coverage
|
||||
/tests.html
|
||||
*.swp
|
||||
|
||||
[build]
|
||||
/tmp/*
|
||||
|
||||
[assets]
|
||||
exclude
|
||||
*.zip
|
||||
*.gz
|
||||
|
||||
[env]
|
||||
/.ruby-version
|
||||
/.grunt/
|
||||
/node_modules
|
||||
/assets
|
||||
/www/dist
|
||||
/www/fixtures
|
||||
/www/font
|
||||
/www/images
|
||||
/www/src
|
||||
/www/vendor
|
||||
/vendor/js/rsvp.js
|
||||
/src/js/config/environments/development_local.js
|
||||
/dist
|
|
@ -0,0 +1,2 @@
|
|||
src/js/main.js
|
||||
src/js/bundles/**/*.js
|
|
@ -0,0 +1,78 @@
|
|||
{
|
||||
"maxerr" : 50,
|
||||
|
||||
"bitwise" : true,
|
||||
"camelcase" : false,
|
||||
"curly" : true,
|
||||
"eqeqeq" : false,
|
||||
"forin" : true,
|
||||
"immed" : false,
|
||||
"indent" : false,
|
||||
"latedef" : true,
|
||||
"newcap" : false,
|
||||
"noarg" : true,
|
||||
"noempty" : true,
|
||||
"nonew" : false,
|
||||
"plusplus" : false,
|
||||
"quotmark" : false,
|
||||
|
||||
"undef" : true,
|
||||
"unused" : true,
|
||||
"strict" : false,
|
||||
"trailing" : false,
|
||||
"maxparams" : false,
|
||||
"maxdepth" : false,
|
||||
"maxstatements" : false,
|
||||
"maxcomplexity" : false,
|
||||
"maxlen" : false,
|
||||
|
||||
"asi" : false,
|
||||
"boss" : false,
|
||||
"debug" : false,
|
||||
"eqnull" : false,
|
||||
"es5" : false,
|
||||
"esnext" : false,
|
||||
"moz" : false,
|
||||
|
||||
"evil" : false,
|
||||
"expr" : false,
|
||||
"funcscope" : false,
|
||||
"globalstrict" : false,
|
||||
"iterator" : false,
|
||||
"lastsemic" : false,
|
||||
"laxbreak" : false,
|
||||
"laxcomma" : false,
|
||||
"loopfunc" : false,
|
||||
"multistr" : false,
|
||||
"proto" : false,
|
||||
"scripturl" : false,
|
||||
"smarttabs" : false,
|
||||
"shadow" : false,
|
||||
"sub" : false,
|
||||
"supernew" : false,
|
||||
"validthis" : false,
|
||||
|
||||
"browser" : true,
|
||||
"couch" : false,
|
||||
"devel" : false,
|
||||
"dojo" : false,
|
||||
"jquery" : false,
|
||||
"mootools" : false,
|
||||
"node" : false,
|
||||
"nonstandard" : false,
|
||||
"prototypejs" : false,
|
||||
"rhino" : false,
|
||||
"worker" : false,
|
||||
"wsh" : false,
|
||||
"yui" : false,
|
||||
|
||||
"nomen" : false,
|
||||
"onevar" : false,
|
||||
"passfail" : false,
|
||||
"white" : false,
|
||||
|
||||
"globals" : {
|
||||
"require": false,
|
||||
"define": false
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"meta": "gray",
|
||||
"reason": "cyan",
|
||||
"verbose": "gray",
|
||||
"error": "red",
|
||||
"noproblem": "green"
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
{
|
||||
"folders": [
|
||||
{
|
||||
"path": ".",
|
||||
"folder_exclude_patterns": [
|
||||
"build",
|
||||
"node_modules",
|
||||
"doc/api",
|
||||
"doc/assets",
|
||||
"doc/src",
|
||||
"doc/vendor",
|
||||
"doc/dist",
|
||||
"./assets",
|
||||
"www/dist",
|
||||
"www/vendor",
|
||||
"www/src",
|
||||
"www/fixtures",
|
||||
".bundle",
|
||||
".git",
|
||||
".grunt",
|
||||
".yardoc",
|
||||
"tmp",
|
||||
"vendor/canvas",
|
||||
".sass-cache"
|
||||
],
|
||||
"file_exclude_patterns": [
|
||||
"*.sublime-workspace"
|
||||
]
|
||||
}
|
||||
],
|
||||
"settings": {
|
||||
"tab_size": 2,
|
||||
"translate_tabs_to_spaces": true,
|
||||
"rulers": [ 80 ],
|
||||
"trim_automatic_white_space": true
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
/* jshint node:true */
|
||||
var grunt = require('grunt');
|
||||
var readPackage = function() {
|
||||
return grunt.file.readJSON('package.json');
|
||||
};
|
||||
|
||||
grunt.loadNpmTasks('grunt-contrib-watch');
|
||||
grunt.loadNpmTasks('grunt-contrib-connect');
|
||||
grunt.loadNpmTasks('grunt-connect-rewrite');
|
||||
grunt.loadNpmTasks('grunt-connect-proxy');
|
||||
grunt.loadNpmTasks('grunt-contrib-jasmine');
|
||||
grunt.loadNpmTasks('grunt-jsduck');
|
||||
grunt.loadNpmTasks('grunt-contrib-jshint');
|
||||
grunt.loadNpmTasks('grunt-notify');
|
||||
grunt.loadNpmTasks('grunt-newer');
|
||||
grunt.loadNpmTasks('grunt-sass');
|
||||
|
||||
grunt.registerTask('default', [
|
||||
'server',
|
||||
'connect:tests',
|
||||
'watch'
|
||||
]);
|
||||
|
||||
grunt.registerTask('updatePkg', function () {
|
||||
grunt.config.set('pkg', readPackage());
|
||||
});
|
||||
|
||||
grunt.util.loadOptions('./tasks/development/options/');
|
||||
grunt.util.loadTasks('./tasks/development');
|
|
@ -0,0 +1,62 @@
|
|||
/* jshint node:true */
|
||||
|
||||
var glob = require('glob');
|
||||
var grunt = require('grunt');
|
||||
|
||||
var readPackage = function() {
|
||||
return grunt.file.readJSON('package.json');
|
||||
};
|
||||
|
||||
var loadOptions = function(path) {
|
||||
glob.sync('*', { cwd: path }).forEach(function(option) {
|
||||
var key = option.replace(/\.js$/,'').replace(/^grunt\-/, '');
|
||||
grunt.config.set(key, require(path + option));
|
||||
});
|
||||
};
|
||||
|
||||
var loadTasks = function(path) {
|
||||
glob.sync('*.js', { cwd: path }).forEach(function(taskFile) {
|
||||
var taskRunner;
|
||||
var task = require(path + '/' + taskFile);
|
||||
var taskName = taskFile.replace(/\.js$/, '');
|
||||
|
||||
taskRunner = task.runner;
|
||||
|
||||
if (taskRunner instanceof Function) {
|
||||
taskRunner = taskRunner.bind(null, grunt);
|
||||
}
|
||||
|
||||
grunt.registerTask(taskName, task.description, taskRunner);
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = function() {
|
||||
'use strict';
|
||||
|
||||
grunt.initConfig({
|
||||
pkg: readPackage()
|
||||
});
|
||||
|
||||
grunt.loadNpmTasks('grunt-contrib-requirejs');
|
||||
grunt.loadNpmTasks('grunt-react');
|
||||
grunt.loadNpmTasks('grunt-contrib-symlink');
|
||||
grunt.loadNpmTasks('grunt-contrib-clean');
|
||||
grunt.loadNpmTasks('grunt-contrib-copy');
|
||||
|
||||
grunt.appName = 'Canvas Quiz Statistics';
|
||||
grunt.moduleId = 'canvas_quiz_statistics';
|
||||
grunt.paths = {
|
||||
canvasPackageShims: 'tmp/canvas_package_shims.json'
|
||||
};
|
||||
|
||||
grunt.util.loadOptions = loadOptions;
|
||||
grunt.util.loadTasks = loadTasks;
|
||||
|
||||
loadOptions('./tasks/options/');
|
||||
loadTasks('./tasks');
|
||||
|
||||
// Unless invoked using `npm run [sub-script] --production`
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
require('./Gruntfile.development');
|
||||
}
|
||||
};
|
|
@ -0,0 +1,661 @@
|
|||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
Version 3, 19 November 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU Affero General Public License is a free, copyleft license for
|
||||
software and other kinds of works, specifically designed to ensure
|
||||
cooperation with the community in the case of network server software.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
our General Public Licenses are intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
Developers that use our General Public Licenses protect your rights
|
||||
with two steps: (1) assert copyright on the software, and (2) offer
|
||||
you this License which gives you legal permission to copy, distribute
|
||||
and/or modify the software.
|
||||
|
||||
A secondary benefit of defending all users' freedom is that
|
||||
improvements made in alternate versions of the program, if they
|
||||
receive widespread use, become available for other developers to
|
||||
incorporate. Many developers of free software are heartened and
|
||||
encouraged by the resulting cooperation. However, in the case of
|
||||
software used on network servers, this result may fail to come about.
|
||||
The GNU General Public License permits making a modified version and
|
||||
letting the public access it on a server without ever releasing its
|
||||
source code to the public.
|
||||
|
||||
The GNU Affero General Public License is designed specifically to
|
||||
ensure that, in such cases, the modified source code becomes available
|
||||
to the community. It requires the operator of a network server to
|
||||
provide the source code of the modified version running there to the
|
||||
users of that server. Therefore, public use of a modified version, on
|
||||
a publicly accessible server, gives the public access to the source
|
||||
code of the modified version.
|
||||
|
||||
An older license, called the Affero General Public License and
|
||||
published by Affero, was designed to accomplish similar goals. This is
|
||||
a different license, not a version of the Affero GPL, but Affero has
|
||||
released a new version of the Affero GPL which permits relicensing under
|
||||
this license.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU Affero General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Remote Network Interaction; Use with the GNU General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, if you modify the
|
||||
Program, your modified version must prominently offer all users
|
||||
interacting with it remotely through a computer network (if your version
|
||||
supports such interaction) an opportunity to receive the Corresponding
|
||||
Source of your version by providing access to the Corresponding Source
|
||||
from a network server at no charge, through some standard or customary
|
||||
means of facilitating copying of software. This Corresponding Source
|
||||
shall include the Corresponding Source for any work covered by version 3
|
||||
of the GNU General Public License that is incorporated pursuant to the
|
||||
following paragraph.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the work with which it is combined will remain governed by version
|
||||
3 of the GNU General Public License.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU Affero General Public License from time to time. Such new versions
|
||||
will be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU Affero General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU Affero General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU Affero General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If your software can interact with users remotely through a computer
|
||||
network, you should also make sure that it provides a way for users to
|
||||
get its source. For example, if your program is a web application, its
|
||||
interface could display a "Source" link that leads users to an archive
|
||||
of the code. There are many ways you could offer source, and different
|
||||
solutions will be better for different programs; see section 13 for the
|
||||
specific requirements.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||
<http://www.gnu.org/licenses/>.
|
|
@ -0,0 +1,28 @@
|
|||
# canvas-quiz-statistics
|
||||
|
||||
A quiz statistics mini-app for Canvas, the LMS by Instructure.
|
||||
|
||||
See the [development guide](https://github.com/amireh/canvas_quiz_statistics/wiki/Development-Guide) to get started.
|
||||
|
||||
## Dependencies
|
||||
|
||||
1. React
|
||||
2. lodash / underscore
|
||||
3. RSVP
|
||||
4. d3
|
||||
|
||||
**Note on lodash**
|
||||
|
||||
Here's a list of the lodash methods that are currently used by CQS:
|
||||
|
||||
* _.compact
|
||||
* _.extend
|
||||
* _.findWhere
|
||||
* _.map
|
||||
* _.merge
|
||||
|
||||
You can generate this list by running `grunt report:lodash_methods`.
|
||||
|
||||
## License
|
||||
|
||||
Released under the AGPLv3 license, like [Canvas](http://github.com/instructure/canvas-lms).
|
|
@ -0,0 +1,5 @@
|
|||
# --------------------------------------------------------------------------- #
|
||||
# DO NOT EDIT. This directory and its contents are auto-generated by JSDuck.
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
To generate the docs, run `grunt docs` in the root directory.
|
|
@ -0,0 +1,54 @@
|
|||
/** @jsx React.DOM */
|
||||
define(function(require) {
|
||||
var React = require('react');
|
||||
var K = require('../../../constants');
|
||||
var Text = require('jsx!../../../components/text');
|
||||
var I18n = require('i18n!quiz_statistics');
|
||||
|
||||
var Help = React.createClass({
|
||||
render: function() {
|
||||
return(
|
||||
<Text
|
||||
scope="discrimination_index_help"
|
||||
articleUrl={K.DISCRIMINATION_INDEX_HELP_ARTICLE_URL}>
|
||||
<p>
|
||||
This metric provides a measure of how well a single question can
|
||||
tell the difference (or discriminate) between students who do
|
||||
well on an exam and those who do not.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
It divides students into three groups based on their score on
|
||||
the whole quiz and displays those groups by who answered the
|
||||
question correctly.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
More information is available
|
||||
<a href="%{article_url}" target="_blank">here</a>.
|
||||
</p>
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
});
|
||||
return Help;
|
||||
});
|
||||
|
||||
// The above is equivalent to writing this:
|
||||
|
||||
I18n.t("discrimination_index_help",
|
||||
"*This metric provides a measure of how well a single question can " +
|
||||
"tell the difference (or discriminate) between students who do well on " +
|
||||
"an exam and those who do not.* " +
|
||||
"**It divides students into three " +
|
||||
"groups based on their score on the whole quiz and displays those " +
|
||||
"groups by who answered the question correctly.** " +
|
||||
"*** More information is available ****here****. ***", {
|
||||
"article_url": K.DISCRIMINATION_INDEX_HELP_ARTICLE_URL,
|
||||
"wrapper": {
|
||||
"****": "<a href=\"%{article_url}\" target=\"_blank\">$1</a>",
|
||||
"***": "<p>$1</p>",
|
||||
"**": "<p>$1</p>",
|
||||
"*": "<p>$1</p>"
|
||||
}
|
||||
})
|
|
@ -0,0 +1,62 @@
|
|||
{
|
||||
"name": "canvas_quiz_statistics",
|
||||
"description": "Quiz statistics app for Canvas the LMS.",
|
||||
"version": "1.0.0",
|
||||
"homepage": "https://github.com/amireh/canvas_quiz_statistics",
|
||||
"author": {
|
||||
"name": "Ahmad Amireh",
|
||||
"email": "ahmad@instructure.com",
|
||||
"url": "http://www.instructure.com"
|
||||
},
|
||||
"licenses": [],
|
||||
"keywords": [
|
||||
"canvas",
|
||||
"lms"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git://github.com/amireh/canvas_quiz_statistics"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/amireh/canvas_quiz_statistics/issues"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "./node_modules/.bin/grunt",
|
||||
"test": "./node_modules/.bin/grunt test",
|
||||
"build": "./node_modules/.bin/grunt compile_js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"node-sass": "^0.9.3",
|
||||
"grunt-contrib-connect": "0.7.x",
|
||||
"grunt-connect-rewrite": "x",
|
||||
"grunt-connect-proxy": "^0.1.10",
|
||||
"grunt-contrib-jasmine": "~0.6.5",
|
||||
"grunt-contrib-jshint": "~0.6.1",
|
||||
"grunt-contrib-watch": "^0.6.1",
|
||||
"grunt-jsduck": "1.x",
|
||||
"grunt-newer": "^0.7.0",
|
||||
"grunt-notify": "0.2.7",
|
||||
"grunt-sass": "^0.14.0",
|
||||
"grunt-template-jasmine-requirejs": "git://github.com/amireh/grunt-template-jasmine-requirejs.git#0.2.0",
|
||||
"jasmine_react": ">= 1.1.0",
|
||||
"jasmine_rsvp": ">= 1.0.0",
|
||||
"jasmine_xhr": ">= 1.0.2",
|
||||
"jquery": ">= 2.0.0",
|
||||
"jshint-stylish-ex": "^0.2.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"glob": "~4.0.5",
|
||||
"shelljs": "~0.1.2",
|
||||
"grunt": "~0.4.1",
|
||||
"grunt-cli": "~0.1.13",
|
||||
"grunt-contrib-clean": "~0.5.0",
|
||||
"grunt-contrib-copy": "~0.5.0",
|
||||
"grunt-contrib-requirejs": "~0.4.4",
|
||||
"grunt-contrib-symlink": "~0.3.0",
|
||||
"grunt-react": "~0.9.0",
|
||||
"lodash": "~2.4.1",
|
||||
"react-tools": "~0.11.1",
|
||||
"canvas_react_i18n": "~ 1.1.0",
|
||||
"rjs_converter": ">= 1.0.0"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,280 @@
|
|||
#question-statistics-section {
|
||||
// The sorting <ic-menu />
|
||||
header ic-menu {
|
||||
position: relative;
|
||||
|
||||
ic-menu-list {
|
||||
top: auto;
|
||||
right: auto;
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.question-statistics {
|
||||
border: 1px solid #CAD0D7;
|
||||
border-radius: 6px;
|
||||
padding: 10px 20px 0;
|
||||
position: relative;
|
||||
margin-bottom: 20px;
|
||||
|
||||
section {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
width: 310px;
|
||||
}
|
||||
|
||||
.question-attempts {
|
||||
font-size: 0.95em;
|
||||
}
|
||||
|
||||
.question-text {
|
||||
margin-top: -5px;
|
||||
margin-bottom: 20px;
|
||||
font-size: 1.25em;
|
||||
}
|
||||
|
||||
aside {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.25s ease-in-out 0.1s;
|
||||
}
|
||||
|
||||
&:hover, &.with-details {
|
||||
background-color: #f4f4f4;
|
||||
|
||||
aside {
|
||||
opacity: 1;
|
||||
pointer-events: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
.correct-answer-ratio-section {
|
||||
.chart, .auxiliary {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.chart {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
|
||||
.background {
|
||||
fill: #ddd;
|
||||
}
|
||||
|
||||
.foreground {
|
||||
fill: $highlightColor;
|
||||
}
|
||||
|
||||
text {
|
||||
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
font-size: 125%;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.auxiliary {
|
||||
margin-left: 20px;
|
||||
width: 160px;
|
||||
}
|
||||
}
|
||||
|
||||
.answer-distribution-section {
|
||||
height: 120px;
|
||||
margin-bottom: -1px;
|
||||
|
||||
.chart {
|
||||
.axis path,
|
||||
.axis line {
|
||||
fill: none;
|
||||
stroke: #000;
|
||||
shape-rendering: crispEdges;
|
||||
}
|
||||
|
||||
.bar {
|
||||
fill: #2a333b;
|
||||
|
||||
&.bar-highlighted {
|
||||
fill: $highlightColor;
|
||||
}
|
||||
|
||||
&.bar-striped {
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
pattern#diagonalStripes {
|
||||
g {
|
||||
stroke: rgba(255,255,255,0.25);
|
||||
stroke-width: 5;
|
||||
}
|
||||
}
|
||||
|
||||
.x.axis path {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.y.axis {
|
||||
fill: $muteTextColor;
|
||||
|
||||
path {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.stretched-answer-distribution .answer-distribution-section {
|
||||
width: 580px;
|
||||
}
|
||||
|
||||
.discrimination-index-section {
|
||||
width: 270px;
|
||||
position: relative;
|
||||
|
||||
.index {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
font-style: normal;
|
||||
|
||||
&.negative {
|
||||
color: red;
|
||||
}
|
||||
|
||||
&.positive {
|
||||
.sign {
|
||||
color: $highlightColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chart {
|
||||
.bar {
|
||||
fill: #2a333b;
|
||||
}
|
||||
.bar.correct {
|
||||
fill: $highlightColor;
|
||||
}
|
||||
}
|
||||
|
||||
.chart-help-trigger {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
color: $muteTextColor;
|
||||
}
|
||||
}
|
||||
|
||||
.essay-score-chart-section {
|
||||
width: 580px;
|
||||
height: 120px;
|
||||
|
||||
path.score-line, circle {
|
||||
stroke: #349e67;
|
||||
shape-rendering: geometricPrecision;
|
||||
image-rendering: optimizeQuality;
|
||||
}
|
||||
|
||||
path.score-line {
|
||||
stroke-width: 4;
|
||||
fill: none;
|
||||
}
|
||||
|
||||
.area {
|
||||
fill: #9ddbbb;
|
||||
}
|
||||
|
||||
circle {
|
||||
stroke-width: 4px;
|
||||
stroke: white;
|
||||
fill: #349e67;
|
||||
}
|
||||
}
|
||||
|
||||
.answer-drilldown {
|
||||
list-style-type: none;
|
||||
margin: 0;
|
||||
padding: 20px 0;
|
||||
border-top: 1px solid #ddd;
|
||||
|
||||
li {
|
||||
display: inline-block;
|
||||
width: 45%;
|
||||
margin-bottom: 20px;
|
||||
min-height: 40px;
|
||||
vertical-align: top;
|
||||
|
||||
&.correct .answer-response-ratio {
|
||||
background-color: $highlightColor;
|
||||
}
|
||||
|
||||
&:nth-of-type(2n) {
|
||||
margin-left: 5%;
|
||||
}
|
||||
}
|
||||
|
||||
.answer-text {
|
||||
margin-left: 55px; /* for the answer response ratio label */
|
||||
}
|
||||
|
||||
.answer-response-ratio {
|
||||
background-color: #2a333b;
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
background: #2a333b;
|
||||
color: white;
|
||||
border-radius: 50% 0 50% 50%;
|
||||
width: 40px;
|
||||
line-height: 40px;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
z-index: 0; /* otherwise selecting the answer text gets crazy */
|
||||
|
||||
sup {
|
||||
font-size: 0.6em;
|
||||
position: absolute;
|
||||
font-weight: normal;
|
||||
top: auto;
|
||||
margin-top: 12px;
|
||||
line-height: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.detail-section {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.with-details .detail-section {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
// Needs to be in global scope because it's placed inside a tooltip
|
||||
.answer-distribution-tooltip-content {
|
||||
text-align: center;
|
||||
|
||||
.answer-response-ratio {
|
||||
font-size: 1.5em;
|
||||
line-height: 2em;
|
||||
font-weight: bold;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.answer-response-count {
|
||||
display: block;
|
||||
font-size: 1.25em;
|
||||
}
|
||||
|
||||
// This might go away if design decides not to embed the answer text in the
|
||||
// tooltip content.
|
||||
hr {
|
||||
height: 1px;
|
||||
line-height: 1px;
|
||||
margin: -5px 0 5px 0;
|
||||
background: none;
|
||||
border: none;
|
||||
box-shadow: 0 1px 0 #333;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
#summary-statistics {
|
||||
$baseFontSize: 24px;
|
||||
$baseLineHeight: 32px;
|
||||
|
||||
.report-generator {
|
||||
.auxiliary {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&+ .report-generator {
|
||||
margin-left: $whitespace;
|
||||
}
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
margin: 20px 40px;
|
||||
}
|
||||
|
||||
th {
|
||||
font-weight: normal;
|
||||
color: $muteTextColor;
|
||||
width: 20%;
|
||||
font-size: $baseFontSize * 0.75;
|
||||
line-height: $baseLineHeight * 0.75;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
td {
|
||||
font-size: $baseFontSize;
|
||||
line-height: $baseLineHeight;
|
||||
|
||||
&.emphasized {
|
||||
font-size: $baseFontSize * 1.25;
|
||||
line-height: $baseLineHeight * 1.25;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.chart {
|
||||
margin-top: 60px;
|
||||
width: 100%;
|
||||
|
||||
.bar {
|
||||
fill: #bedff1;
|
||||
}
|
||||
|
||||
path.median-dist-graph {
|
||||
shape-rendering: geometricPrecision;
|
||||
image-rendering: optimizeQuality;
|
||||
stroke-width: 2;
|
||||
stroke: #2894d1;
|
||||
fill: none;
|
||||
opacity: 0.1;
|
||||
}
|
||||
|
||||
.x.axis {
|
||||
fill: $muteTextColor;
|
||||
|
||||
line, path {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.report-generator.busy {
|
||||
.btn {
|
||||
background-color: transparent;
|
||||
color: #333;
|
||||
opacity: 0.65;
|
||||
cursor: default;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
$highlightColor: #349e67;
|
||||
$muteTextColor: #949494;
|
||||
$whitespace: 10px;
|
|
@ -0,0 +1,64 @@
|
|||
@import "sass-functions/ensure-contrast";
|
||||
@import "base/variables";
|
||||
@import "./variables";
|
||||
@import "./summary_statistics";
|
||||
@import "./question_statistics";
|
||||
|
||||
#canvas-quiz-statistics {
|
||||
border-top: 1px solid #CAD0D7;
|
||||
padding: 25px 35px 55px 35px;
|
||||
|
||||
header {
|
||||
line-height: 34px;
|
||||
|
||||
&.padded {
|
||||
padding: 20px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.inline {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
line-height: 1;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.answer-set-tabs {
|
||||
margin-top: -10px; // negate the margin in the <p /> in the header above
|
||||
margin-bottom: 20px;
|
||||
|
||||
button {
|
||||
display: inline-block;
|
||||
color: white;
|
||||
background: $muteTextColor;
|
||||
font-weight: bold;
|
||||
padding: 5px 10px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
|
||||
&.active, &:hover {
|
||||
background: #2894d1;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.erratic-statistics {
|
||||
text-align: center;
|
||||
margin-top: 20px;
|
||||
|
||||
img {
|
||||
height: 35px;
|
||||
width: 218px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-weight: bold;
|
||||
margin-bottom: 0;
|
||||
line-height: 22px;
|
||||
font-size: 14.5px;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
define(function(require) {
|
||||
var VERSION = require('./version');
|
||||
var config = require('./config');
|
||||
var delegate = require('./core/delegate');
|
||||
var exports = {};
|
||||
|
||||
exports.configure = delegate.configure;
|
||||
exports.mount = delegate.mount;
|
||||
exports.isMounted = delegate.isMounted;
|
||||
exports.update = delegate.update;
|
||||
exports.reload = delegate.reload;
|
||||
exports.unmount = delegate.unmount;
|
||||
exports.version = VERSION;
|
||||
exports.config = config;
|
||||
|
||||
return exports;
|
||||
});
|
|
@ -0,0 +1,11 @@
|
|||
define(function(require) {
|
||||
var Backbone = require('canvas_packages/backbone');
|
||||
var QuizReport = require('../models/quiz_report');
|
||||
|
||||
return Backbone.Collection.extend({
|
||||
model: QuizReport,
|
||||
parse: function(payload) {
|
||||
return payload.quiz_reports;
|
||||
}
|
||||
});
|
||||
});
|
|
@ -0,0 +1,11 @@
|
|||
define(function(require) {
|
||||
var Backbone = require('canvas_packages/backbone');
|
||||
var QuizStatistics = require('../models/quiz_statistics');
|
||||
|
||||
return Backbone.Collection.extend({
|
||||
model: QuizStatistics,
|
||||
parse: function(payload) {
|
||||
return payload.quiz_statistics;
|
||||
}
|
||||
});
|
||||
});
|
|
@ -0,0 +1,4 @@
|
|||
=== Components
|
||||
|
||||
Any view that is being used in more than one place should go here.
|
||||
Page-specific views go under `/views` instead.
|
|
@ -0,0 +1,223 @@
|
|||
/** @jsx React.DOM */
|
||||
define(function(require) {
|
||||
var React = require('react');
|
||||
var jQueryUIDialog = require('canvas_packages/jqueryui/dialog');
|
||||
var $ = require('canvas_packages/jquery');
|
||||
var _ = require('lodash');
|
||||
var omit = _.omit;
|
||||
|
||||
/**
|
||||
* @class Components.Dialog
|
||||
*
|
||||
* Wrap a component inside a jQueryUI Dialog and keep it updated like you
|
||||
* would any other component. The wrapped component is refered to as the
|
||||
* "content", while the wrapper component which you interact with is refered
|
||||
* to as the "Dialog".
|
||||
*
|
||||
* All the props you pass to this component are passed through as-is to the
|
||||
* dialog content, except for a number of props that control the dialog's
|
||||
* toggle button. See #propTypes for more info.
|
||||
*
|
||||
* TODO: a11y
|
||||
*
|
||||
* === Usage example
|
||||
*
|
||||
* Let's say you have a view called Help that has some helpful information
|
||||
* and you would like to display this view inside a dialog. You also want
|
||||
* to bind a button to show this dialog, with a label that says "Help".
|
||||
*
|
||||
* define(function(require) {
|
||||
* var Dialog = require('jsx!components/dialog');
|
||||
* var HelpView = require('jsx!./help');
|
||||
*
|
||||
* var View = React.createClass({
|
||||
* render: function() {
|
||||
* return (
|
||||
* <div>
|
||||
* <Dialog
|
||||
* content={HelpView}
|
||||
* tagName="button"
|
||||
* className="btn btn-success">
|
||||
* Help
|
||||
* </Dialog>
|
||||
* </div>
|
||||
* );
|
||||
* }
|
||||
* });
|
||||
*
|
||||
* return View;
|
||||
* });
|
||||
*/
|
||||
var Dialog = React.createClass({
|
||||
propTypes: {
|
||||
/**
|
||||
* @property {React.Component} content
|
||||
*
|
||||
* A type of component that should be rendered *inside* the $.dialog().
|
||||
*
|
||||
* The Dialog component will take care of mounting an instance of this
|
||||
* type inside the $.dialog() and keeping it updated with the props you
|
||||
* pass through.
|
||||
*/
|
||||
content: React.PropTypes.func,
|
||||
|
||||
/**
|
||||
* @property {React.Component} children
|
||||
*
|
||||
* Whatever you pass as children to this component will act as a toggle
|
||||
* button for the dialog. Clicking it will show or hide the dialog based
|
||||
* on its state.
|
||||
*
|
||||
* You can choose to pass nothing for this, then you will have to manually
|
||||
* control the toggling of the dialog by assigning a ref and using the
|
||||
* exposed API. Example:
|
||||
*
|
||||
* render: function() {
|
||||
* return (
|
||||
* <div onClick={this.toggleDialog}>
|
||||
* <Dialog content={MyContent} ref="dialog" />
|
||||
* </div>
|
||||
* )
|
||||
* },
|
||||
*
|
||||
* toggleDialog: function() {
|
||||
* if (this.refs.dialog.isOpen()) {
|
||||
* this.refs.dialog.close();
|
||||
* } else {
|
||||
* this.refs.dialog.open();
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
children: React.PropTypes.renderable,
|
||||
|
||||
/**
|
||||
* @property {String} [tagName="div"]
|
||||
* You can customize the tag that is used as the dialog toggle element.
|
||||
*/
|
||||
tagName: React.PropTypes.string,
|
||||
|
||||
/**
|
||||
* @property {String} [className=""]
|
||||
* CSS classes to add to the dialog toggle element.
|
||||
*/
|
||||
className: React.PropTypes.string
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
content: null,
|
||||
container: null,
|
||||
$container: null
|
||||
};
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
children: [],
|
||||
autoOpen: false,
|
||||
tagName: 'div'
|
||||
};
|
||||
},
|
||||
|
||||
componentDidUpdate: function(/*prevProps, prevState*/) {
|
||||
var props = this.props;
|
||||
|
||||
// Create the dialog if it hasn't been created yet:
|
||||
if (!this.state.content && props.content) {
|
||||
this.__renderDialog(props.content, props);
|
||||
}
|
||||
|
||||
// Update the component within the dialog:
|
||||
if (this.state.content) {
|
||||
this.state.content.setProps(this.__getContentProps(props));
|
||||
}
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
this.__removeDialog();
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var tag = React.DOM[this.props.tagName];
|
||||
|
||||
return (
|
||||
<tag
|
||||
onClick={this.toggle}
|
||||
className={this.props.className}
|
||||
children={this.props.children} />
|
||||
);
|
||||
},
|
||||
|
||||
/** Open the dialog */
|
||||
open: function() {
|
||||
this.__send('open');
|
||||
},
|
||||
|
||||
/** Close the dialog */
|
||||
close: function() {
|
||||
this.__send('close');
|
||||
},
|
||||
|
||||
/** Is the dialog open? */
|
||||
isOpen: function() {
|
||||
return this.__send('isOpen');
|
||||
},
|
||||
|
||||
toggle: function() {
|
||||
if (this.isOpen()) {
|
||||
this.close();
|
||||
} else {
|
||||
this.open();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
__renderDialog: function(content, props) {
|
||||
var container = document.createElement('div');
|
||||
var renderedContent = React.renderComponent(content(), container);
|
||||
|
||||
$(container).dialog({
|
||||
autoOpen: props.autoOpen
|
||||
});
|
||||
|
||||
this.setState({
|
||||
content: renderedContent,
|
||||
container: container,
|
||||
$container: $(container)
|
||||
});
|
||||
},
|
||||
|
||||
__removeDialog: function() {
|
||||
if (this.state.$container) {
|
||||
// No need to remove the container as it was not really attached to
|
||||
// the DOM, simply unmounting the component will suffice.
|
||||
React.unmountComponentAtNode(this.state.container);
|
||||
|
||||
this.state.$container.dialog('destroy');
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @internal Send an API command to the jQueryUI Dialog instance.
|
||||
*
|
||||
* @param {String} command
|
||||
* jQueryUI Dialog API message.
|
||||
*
|
||||
* @return {Mixed}
|
||||
* Whatever the dialog API returns, if a dialog actually exists.
|
||||
*/
|
||||
__send: function(command) {
|
||||
if (this.state.$container) {
|
||||
return this.state.$container.dialog(command);
|
||||
}
|
||||
},
|
||||
|
||||
__getContentProps: function(props) {
|
||||
return omit(props, [ 'className', 'tagName', 'content', 'children' ]);
|
||||
}
|
||||
});
|
||||
|
||||
return Dialog;
|
||||
});
|
|
@ -0,0 +1,113 @@
|
|||
/** @jsx React.DOM */
|
||||
define(function(require) {
|
||||
var React = require('react');
|
||||
var ChartMixin = require('../mixins/chart');
|
||||
var d3 = require('d3');
|
||||
var max = d3.max;
|
||||
var sum = d3.sum;
|
||||
|
||||
var MARGIN_T = 0;
|
||||
var MARGIN_R = 0;
|
||||
var MARGIN_B = 40;
|
||||
var MARGIN_L = -40;
|
||||
var WIDTH = 960;
|
||||
var HEIGHT = 220;
|
||||
var BAR_WIDTH = 10;
|
||||
var BAR_MARGIN = 0.25;
|
||||
|
||||
var ScorePercentileChart = React.createClass({
|
||||
mixins: [ ChartMixin.mixin ],
|
||||
|
||||
propTypes: {
|
||||
scores: React.PropTypes.object.isRequired
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
scores: {}
|
||||
};
|
||||
},
|
||||
|
||||
createChart: function(node, props) {
|
||||
var height, highest, svg, width, x, xAxis, y;
|
||||
var data = this.chartData(props);
|
||||
|
||||
width = WIDTH - MARGIN_L - MARGIN_R;
|
||||
height = HEIGHT - MARGIN_T - MARGIN_B;
|
||||
highest = max(data);
|
||||
|
||||
x = d3.scale.ordinal().rangeRoundBands([0, BAR_WIDTH * data.length], BAR_MARGIN);
|
||||
y = d3.scale.linear().range([0, highest]).rangeRound([height, 0]);
|
||||
|
||||
x.domain(data.map(function(d, i) {
|
||||
return i;
|
||||
}));
|
||||
|
||||
y.domain([0, highest]);
|
||||
|
||||
xAxis = d3.svg.axis().scale(x).orient("bottom").tickValues(d3.range(0, 101, 10)).tickFormat(function(d) {
|
||||
return d + '%';
|
||||
});
|
||||
|
||||
svg = d3.select(node)
|
||||
.attr('width', width + MARGIN_L + MARGIN_R)
|
||||
.attr('height', height + MARGIN_T + MARGIN_B)
|
||||
.attr('viewBox', "0 0 " + (width + MARGIN_L + MARGIN_R) + " " + (height + MARGIN_T + MARGIN_B))
|
||||
.attr('preserveAspectRatio', 'xMinYMax')
|
||||
.append('g')
|
||||
.attr("transform", "translate(" + MARGIN_L + "," + MARGIN_T + ")");
|
||||
|
||||
svg.append('g')
|
||||
.attr('class', 'x axis')
|
||||
.attr('transform', "translate(0," + height + ")")
|
||||
.call(xAxis);
|
||||
|
||||
this.renderPercentileChart(svg, data, x, y, height);
|
||||
|
||||
return svg;
|
||||
},
|
||||
|
||||
renderPercentileChart: function(svg, data, x, y, height) {
|
||||
var highest, visibilityThreshold;
|
||||
|
||||
highest = y.domain()[1];
|
||||
|
||||
visibilityThreshold = Math.min(highest / 100, 0.5);
|
||||
|
||||
svg.selectAll('rect.bar')
|
||||
.data(data)
|
||||
.enter()
|
||||
.append('rect')
|
||||
.attr("class", 'bar')
|
||||
.attr('x', function(d, i) {
|
||||
return x(i);
|
||||
}).attr('width', x.rangeBand).attr('y', function(d) {
|
||||
return y(d + visibilityThreshold);
|
||||
}).attr('height', function(d) {
|
||||
return height - y(d + visibilityThreshold);
|
||||
});
|
||||
},
|
||||
|
||||
chartData: function(props) {
|
||||
var percentile, upperBound;
|
||||
var set = [];
|
||||
var scores = props.scores || {};
|
||||
var highest = max(Object.keys(scores).map(function(score) {
|
||||
return parseInt(score, 10);
|
||||
}));
|
||||
|
||||
upperBound = max([100, highest]);
|
||||
|
||||
for (percentile = 0; percentile < upperBound; ++percentile) {
|
||||
set[percentile] = scores[''+percentile] || 0;
|
||||
}
|
||||
|
||||
// merge right outliers with 100%
|
||||
set[100] = sum(set.splice(100, set.length));
|
||||
|
||||
return set;
|
||||
}
|
||||
});
|
||||
|
||||
return ScorePercentileChart;
|
||||
});
|
|
@ -0,0 +1,67 @@
|
|||
/** @jsx React.DOM */
|
||||
define(function(require) {
|
||||
var React = require('react');
|
||||
var _ = require('lodash');
|
||||
var interpolate = require('../util/i18n_interpolate');
|
||||
var convertCase = require('../util/convert_case');
|
||||
|
||||
var omit = _.omit;
|
||||
var underscore = convertCase.underscore;
|
||||
|
||||
var InterpolatedText = React.createClass({
|
||||
render: function() {
|
||||
var container, markup, tagAttrs, options;
|
||||
if (!this.props.children) {
|
||||
return <div />;
|
||||
}
|
||||
|
||||
tagAttrs = {};
|
||||
container = <div>{this.props.children}</div>;
|
||||
markup = React.renderComponentToStaticMarkup(container);
|
||||
options = omit(this.props, 'children');
|
||||
|
||||
tagAttrs.dangerouslySetInnerHTML = {
|
||||
__html: interpolate(markup, underscore(options || {}))
|
||||
};
|
||||
|
||||
return(
|
||||
React.DOM.div(tagAttrs)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var Text = React.createClass({
|
||||
getInitialState: function() {
|
||||
return {
|
||||
markup: undefined
|
||||
};
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
scope: null,
|
||||
};
|
||||
},
|
||||
|
||||
//>>excludeStart("production", pragmas.production);
|
||||
componentWillReceiveProps: function(nextProps) {
|
||||
var markup;
|
||||
|
||||
if (nextProps.scope) {
|
||||
markup = React.renderComponentToStaticMarkup(InterpolatedText(nextProps));
|
||||
markup = markup.replace(/<\/?div>/g, '');
|
||||
|
||||
this.setState({
|
||||
markup: markup
|
||||
});
|
||||
}
|
||||
},
|
||||
//>>excludeEnd("production");
|
||||
|
||||
render: function() {
|
||||
return <div dangerouslySetInnerHTML={{__html: this.state.markup }} />;
|
||||
}
|
||||
});
|
||||
|
||||
return Text;
|
||||
});
|
|
@ -0,0 +1,28 @@
|
|||
define([
|
||||
'require',
|
||||
'lodash',
|
||||
'./config/environments/production',
|
||||
], function(require, _, ProductionConfig) {
|
||||
var config = ProductionConfig || {};
|
||||
|
||||
//>>excludeStart("production", pragmas.production);
|
||||
// Install development and local config:
|
||||
require([
|
||||
'./config/environments/development',
|
||||
'./config/environments/development_local'
|
||||
], function(devConfig, localConfig) {
|
||||
config = _.extend(config, devConfig, localConfig);
|
||||
}, function(e) {
|
||||
if (e.requireType === 'scripterror') {
|
||||
// don't whine if the files don't exist:
|
||||
console.info(
|
||||
'Hint: you can set up your own private, development-only configuration in',
|
||||
'"config/environments/development_local.js".');
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
//>>excludeEnd("production");
|
||||
|
||||
return config;
|
||||
});
|
|
@ -0,0 +1,40 @@
|
|||
# Environment initializers
|
||||
|
||||
Modules in this directory are expected to return a JSON object that contains
|
||||
initial app configuration.
|
||||
|
||||
## `production.js`
|
||||
|
||||
Contains initial configuration for the built-version of CQS, which will be
|
||||
embedded inside Canvas.
|
||||
|
||||
## `development.js`
|
||||
|
||||
This file will *not* be included in the built version. Use this to define
|
||||
helpers helpers and config defaults that facilitate development. This will be
|
||||
checked in to git and shared between the team.
|
||||
|
||||
## `development_local.js`
|
||||
|
||||
This file will not be included in the built version, nor will it be checked-in
|
||||
to git. Use this file to provide your own API token and any "private" config
|
||||
that you don't necessarily think other team members will be benefit from.
|
||||
|
||||
This file is also a good place to swap in fixtures instead of API endpoints if
|
||||
you want to speed things up and not hit the actual Canvas API. For example:
|
||||
|
||||
```javascript
|
||||
return {
|
||||
apiToken: 'MY_API_TOKEN',
|
||||
|
||||
// You can hit against the actual Canvas API if you got reverse proxy
|
||||
// going on:
|
||||
//
|
||||
// quizStatisticsUrl: '/api/v1/courses/1/quizzes/1/statistics',
|
||||
// quizReportsUrl: '/api/v1/courses/1/quizzes/1/reports',
|
||||
|
||||
// Or just use the fixtures for speed:
|
||||
quizStatisticsUrl: '/fixtures/quiz_statistics_all_types.json',
|
||||
quizReportsUrl: '/fixtures/quiz_reports.json',
|
||||
};
|
||||
```
|
|
@ -0,0 +1,42 @@
|
|||
define(function(require) {
|
||||
var rawAjax = require('../../util/xhr_request');
|
||||
var Root = this;
|
||||
var DEBUG = {
|
||||
};
|
||||
|
||||
DEBUG.expose = function(script, varName) {
|
||||
require([ script ], function(__script__) {
|
||||
DEBUG[varName] = __script__;
|
||||
});
|
||||
};
|
||||
|
||||
require([ 'boot' ], function(app) {
|
||||
DEBUG.app = app;
|
||||
DEBUG.update = app.update;
|
||||
});
|
||||
|
||||
DEBUG.expose('react', 'React');
|
||||
DEBUG.expose('util/round', 'round');
|
||||
DEBUG.expose('stores/statistics', 'statisticsStore');
|
||||
|
||||
Root.DEBUG = DEBUG;
|
||||
Root.d = DEBUG;
|
||||
|
||||
return {
|
||||
xhr: {
|
||||
timeout: 5000
|
||||
},
|
||||
|
||||
ajax: rawAjax,
|
||||
|
||||
// This assumes you have set up reverse proxying on /api/v1 to Canvas.
|
||||
//
|
||||
// See ./README.md for more info on overriding these to use fixtures.
|
||||
quizStatisticsUrl: '/api/v1/courses/1/quizzes/1/statistics',
|
||||
quizReportsUrl: '/api/v1/courses/1/quizzes/1/reports',
|
||||
|
||||
onError: function(message) {
|
||||
throw new Error(message);
|
||||
}
|
||||
};
|
||||
});
|
|
@ -0,0 +1,68 @@
|
|||
define([], function() {
|
||||
/**
|
||||
* @class Config
|
||||
* Application-wide configuration.
|
||||
*
|
||||
* Some parameters are required to be set up correctly before the app is
|
||||
* mounted for it to work properly.
|
||||
*
|
||||
* === Example: configuring the app
|
||||
*
|
||||
* require([ 'path/to/app' ], function(app) {
|
||||
* app.configure({
|
||||
* precision: 2,
|
||||
* ajax: $.ajax
|
||||
* });
|
||||
* });
|
||||
*/
|
||||
return {
|
||||
/**
|
||||
* @cfg {Number} [precision=2]
|
||||
*
|
||||
* Number of decimals to round to when displaying floats.
|
||||
*/
|
||||
precision: 2,
|
||||
|
||||
/**
|
||||
* @cfg {Function} ajax
|
||||
* An XHR request processor that has an API compatible with jQuery.ajax.
|
||||
*/
|
||||
ajax: undefined,
|
||||
|
||||
/**
|
||||
* @cfg {String} quizStatisticsUrl
|
||||
* Canvas API endpoint for querying the current quiz's statistics.
|
||||
*/
|
||||
quizStatisticsUrl: undefined,
|
||||
|
||||
/**
|
||||
* @cfg {String} quizReportsUrl
|
||||
* Canvas API endpoint for querying the current quiz's statistic reports.
|
||||
*/
|
||||
quizReportsUrl: undefined,
|
||||
|
||||
/**
|
||||
* @cfg {Boolean} [loadOnStartup=true]
|
||||
*
|
||||
* Whether the app should query all the data it needs as soon as it is
|
||||
* mounted.
|
||||
*
|
||||
* You may disable this behavior if you want to manually inject the app
|
||||
* with data.
|
||||
*/
|
||||
loadOnStartup: true,
|
||||
|
||||
/**
|
||||
* Error emitter. Default behavior is to log the error message to the
|
||||
* console.
|
||||
*
|
||||
* Override this to handle errors from the app.
|
||||
*
|
||||
* @param {String} message
|
||||
* An explanation of the error.
|
||||
*/
|
||||
onError: function(message) {
|
||||
console.error(message);
|
||||
}
|
||||
};
|
||||
});
|
|
@ -0,0 +1,8 @@
|
|||
define(function(require) {
|
||||
var d3 = require('./initializers/d3');
|
||||
var RSVP = require('./initializers/rsvp');
|
||||
|
||||
return function initializeApp() {
|
||||
return RSVP.resolve();
|
||||
};
|
||||
});
|
|
@ -0,0 +1,4 @@
|
|||
# Initializers
|
||||
|
||||
Files in this folder are expected to configure 3rd-party libraries, quite
|
||||
similarily to Rails initializers.
|
|
@ -0,0 +1,3 @@
|
|||
define([ 'd3' ], function() {
|
||||
'use strict';
|
||||
});
|
|
@ -0,0 +1,14 @@
|
|||
define([ 'rsvp' ], function(RSVP) {
|
||||
RSVP.on('error', function(e) {
|
||||
console.error('RSVP error:', e);
|
||||
|
||||
if (e && e.message) {
|
||||
console.error(e.message);
|
||||
}
|
||||
if (e && e.stack) {
|
||||
console.error(e.stack);
|
||||
}
|
||||
});
|
||||
|
||||
return RSVP;
|
||||
});
|
|
@ -0,0 +1,67 @@
|
|||
define(function() {
|
||||
return {
|
||||
DISCRIMINATION_INDEX_THRESHOLD: 0.25,
|
||||
|
||||
// a whitelist of the attributes we need from the payload
|
||||
QUIZ_STATISTICS_ATTRS: [
|
||||
'id',
|
||||
'points_possible',
|
||||
'speed_grader_url',
|
||||
'quiz_submissions_zip_url',
|
||||
],
|
||||
|
||||
SUBMISSION_STATISTICS_ATTRS: [
|
||||
'score_average',
|
||||
'score_high',
|
||||
'score_low',
|
||||
'score_stdev',
|
||||
'scores',
|
||||
'duration_average',
|
||||
'unique_count',
|
||||
],
|
||||
|
||||
QUESTION_STATISTICS_ATTRS: [
|
||||
'id',
|
||||
'question_type',
|
||||
'question_text',
|
||||
'responses',
|
||||
'answers',
|
||||
'answered_student_count',
|
||||
|
||||
'top_student_count',
|
||||
'middle_student_count',
|
||||
'bottom_student_count',
|
||||
'correct_top_student_count',
|
||||
'correct_middle_student_count',
|
||||
'correct_bottom_student_count',
|
||||
'point_biserials'
|
||||
],
|
||||
|
||||
POINT_BISERIAL_ATTRS: [
|
||||
'answer_id',
|
||||
'correct',
|
||||
'distractor',
|
||||
'point_biserial',
|
||||
],
|
||||
|
||||
QUIZ_REPORT_ATTRS: [
|
||||
'id',
|
||||
'report_type',
|
||||
'readable_type',
|
||||
'generatable'
|
||||
],
|
||||
|
||||
PROGRESS_ATTRS: [
|
||||
'id',
|
||||
'completion',
|
||||
'url', // for polling
|
||||
'workflow_state'
|
||||
],
|
||||
|
||||
ATTACHMENT_ATTRS: [
|
||||
'url'
|
||||
],
|
||||
|
||||
DISCRIMINATION_INDEX_HELP_ARTICLE_URL: "http://guides.instructure.com/m/4152/l/41484-once-i-publish-my-quiz-what-kinds-of-quiz-statistics-are-available"
|
||||
};
|
||||
});
|
|
@ -0,0 +1,23 @@
|
|||
define(function(require) {
|
||||
var rawAjax = require('../util/xhr_request');
|
||||
var config = require('../config');
|
||||
var RSVP = require('rsvp');
|
||||
|
||||
var Adapter = {
|
||||
request: function(options) {
|
||||
var ajax = config.ajax || rawAjax;
|
||||
|
||||
options.headers = options.headers || {};
|
||||
options.headers['Content-Type'] = 'application/json';
|
||||
options.headers['Accept'] = 'application/vnd.api+json';
|
||||
|
||||
if (config.apiToken) {
|
||||
options.headers.Authorization = 'Bearer ' + config.apiToken;
|
||||
}
|
||||
|
||||
return RSVP.Promise.cast(ajax(options));
|
||||
}
|
||||
};
|
||||
|
||||
return Adapter;
|
||||
});
|
|
@ -0,0 +1,73 @@
|
|||
define(function(require) {
|
||||
var statisticsStore = require('../stores/statistics');
|
||||
var config = require('../config');
|
||||
var update;
|
||||
|
||||
var onChange = function() {
|
||||
update({
|
||||
quizStatistics: statisticsStore.getQuizStatistics(),
|
||||
quizReports: statisticsStore.getQuizReports(),
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @class Core.Controller
|
||||
* @private
|
||||
*
|
||||
* The controller is responsible for keeping the UI up-to-date with the
|
||||
* data layer.
|
||||
*/
|
||||
var Controller = {
|
||||
|
||||
/**
|
||||
* Start listening to data updates.
|
||||
*
|
||||
* @param {Function} onUpdate
|
||||
* A callback to notify when new data comes in.
|
||||
*
|
||||
* @param {Object} onUpdate.props
|
||||
* A set of props ready for injecting into the app layout.
|
||||
*
|
||||
* @param {Object} onUpdate.props.quizStatistics
|
||||
* Quiz statistics.
|
||||
* See Stores.Statistics#getQuizStatistics().
|
||||
*
|
||||
* @param {Object} onUpdate.props.quizReports
|
||||
* Quiz reports.
|
||||
* See Stores.Statistics#getQuizReports().
|
||||
*/
|
||||
start: function(onUpdate) {
|
||||
update = onUpdate;
|
||||
statisticsStore.addChangeListener(onChange);
|
||||
|
||||
if (config.loadOnStartup) {
|
||||
Controller.load();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Load initial application data; quiz statistics and reports.
|
||||
*/
|
||||
load: function() {
|
||||
if (config.quizStatisticsUrl) {
|
||||
statisticsStore.load();
|
||||
}
|
||||
else {
|
||||
console.warn(
|
||||
'You have requested to load on start-up, but have not',
|
||||
'provided a url to load from in CQS.config.quizStatisticsUrl.'
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Stop listening to data changes.
|
||||
*/
|
||||
stop: function() {
|
||||
statisticsStore.removeChangeListener(onChange);
|
||||
update = undefined;
|
||||
}
|
||||
};
|
||||
|
||||
return Controller;
|
||||
});
|
|
@ -0,0 +1,80 @@
|
|||
define(function(require) {
|
||||
var React = require('react');
|
||||
var _ = require('lodash');
|
||||
var config = require('../config');
|
||||
var initialize = require('../config/initializer');
|
||||
var Layout = require('jsx!../views/app');
|
||||
var controller = require('./controller');
|
||||
var extend = _.extend;
|
||||
var container;
|
||||
var layout;
|
||||
|
||||
/**
|
||||
* @class Core.Delegate
|
||||
*
|
||||
* The client app delegate. This is the main interface that embedding
|
||||
* applications use to interact with the client app.
|
||||
*/
|
||||
var exports = {};
|
||||
|
||||
/**
|
||||
* Configure the application. See Config for the supported options.
|
||||
*
|
||||
* @param {Object} options
|
||||
* A set of options to override.
|
||||
*/
|
||||
var configure = function(options) {
|
||||
extend(config, options);
|
||||
};
|
||||
|
||||
/**
|
||||
* Start the app and perform any necessary data loading.
|
||||
*
|
||||
* @param {HTMLElement} node
|
||||
* The node to mount the app in.
|
||||
*
|
||||
* @param {Object} [options={}]
|
||||
* Options to configure the app with. See config.js
|
||||
*
|
||||
* @return {RSVP.Promise}
|
||||
* Fulfilled when the app has been started and rendered.
|
||||
*/
|
||||
var mount = function(node, options) {
|
||||
configure(options);
|
||||
container = node;
|
||||
|
||||
return initialize().then(function() {
|
||||
layout = React.renderComponent(Layout(), container);
|
||||
controller.start(update);
|
||||
});
|
||||
};
|
||||
|
||||
var isMounted = function() {
|
||||
return !!layout;
|
||||
};
|
||||
|
||||
var update = function(props) {
|
||||
layout.setProps(props);
|
||||
};
|
||||
|
||||
var reload = function() {
|
||||
controller.load();
|
||||
};
|
||||
|
||||
var unmount = function() {
|
||||
if (isMounted()) {
|
||||
controller.stop();
|
||||
React.unmountComponentAtNode(container);
|
||||
container = undefined;
|
||||
}
|
||||
};
|
||||
|
||||
exports.configure = configure;
|
||||
exports.mount = mount;
|
||||
exports.isMounted = isMounted;
|
||||
exports.update = update;
|
||||
exports.reload = reload;
|
||||
exports.unmount = unmount;
|
||||
|
||||
return exports;
|
||||
});
|
|
@ -0,0 +1,41 @@
|
|||
define(function(require) {
|
||||
var RSVP = require('rsvp');
|
||||
var singleton;
|
||||
var callbacks = {};
|
||||
var gActionIndex = 0;
|
||||
|
||||
var Dispatcher = function() {
|
||||
return this;
|
||||
};
|
||||
|
||||
Dispatcher.prototype.dispatch = function(action, params) {
|
||||
var service = RSVP.defer();
|
||||
var actionIndex = ++gActionIndex;
|
||||
var callback = callbacks[action];
|
||||
|
||||
if (callback) {
|
||||
callback(params, service.resolve, service.reject);
|
||||
}
|
||||
else {
|
||||
console.assert(false, 'No action handler registered to:', action);
|
||||
service.reject('Unknown action "' + action + '"');
|
||||
}
|
||||
|
||||
return {
|
||||
promise: service.promise,
|
||||
index: actionIndex
|
||||
};
|
||||
};
|
||||
|
||||
Dispatcher.prototype.register = function(action, callback) {
|
||||
if (callbacks[action]) {
|
||||
throw new Error("A handler is already registered to '" + action + "'");
|
||||
}
|
||||
|
||||
callbacks[action] = callback;
|
||||
};
|
||||
|
||||
singleton = new Dispatcher();
|
||||
|
||||
return singleton;
|
||||
});
|
|
@ -0,0 +1,69 @@
|
|||
define(function(require) {
|
||||
var _ = require('lodash');
|
||||
var Dispatcher = require('./dispatcher');
|
||||
var extend = _.extend;
|
||||
|
||||
var Store = function(key, proto) {
|
||||
var emitChange = this.emitChange.bind(this);
|
||||
|
||||
this._key = key;
|
||||
this.__reset__();
|
||||
|
||||
extend(this, proto || {});
|
||||
|
||||
Object.keys(this.actions).forEach(function(action) {
|
||||
var handler = this.actions[action].bind(this);
|
||||
var scopedAction = [ key, action ].join(':');
|
||||
|
||||
console.debug('Store action:', scopedAction);
|
||||
|
||||
Dispatcher.register(scopedAction, function(params, resolve, reject) {
|
||||
try {
|
||||
handler(params, function onChange(rc) {
|
||||
resolve(rc);
|
||||
emitChange();
|
||||
}, reject);
|
||||
} catch(e) {
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
|
||||
}.bind(this));
|
||||
|
||||
return this;
|
||||
};
|
||||
|
||||
extend(Store.prototype, {
|
||||
actions: {},
|
||||
addChangeListener: function(callback) {
|
||||
this._callbacks.push(callback);
|
||||
},
|
||||
|
||||
removeChangeListener: function(callback) {
|
||||
var index = this._callbacks.indexOf(callback);
|
||||
if (index > -1) {
|
||||
this._callbacks.splice(index, 1);
|
||||
}
|
||||
},
|
||||
|
||||
emitChange: function() {
|
||||
this._callbacks.forEach(function(callback) {
|
||||
callback();
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* A hook for tests to reset the Store to its initial state. Override this
|
||||
* to restore any side-effects.
|
||||
*
|
||||
* Usually during the life-time of the app, we will never have to reset a
|
||||
* Store, but in tests we do.
|
||||
*/
|
||||
__reset__: function() {
|
||||
this._callbacks = [];
|
||||
}
|
||||
});
|
||||
|
||||
return Store;
|
||||
});
|
|
@ -0,0 +1,9 @@
|
|||
`ext/` contains extensions modules to external packages.
|
||||
|
||||
This may include helpers and plugins, and configuration of those packages as well.
|
||||
|
||||
If a package has several extensions, it is better to create an extension entry
|
||||
point named by `ext/package.js` which will in turn include all the available
|
||||
extension packages. However, this might not always be applicable, so feel free
|
||||
to mix and match, but you should always create a package folder for any
|
||||
package that contains more than one extension.
|
|
@ -0,0 +1,12 @@
|
|||
define(function(require) {
|
||||
var React = require('react');
|
||||
var ActorMixin = require('../mixins/components/actor');
|
||||
|
||||
if (!React.addons) {
|
||||
React.addons = {};
|
||||
}
|
||||
|
||||
React.addons.ActorMixin = ActorMixin;
|
||||
|
||||
return React;
|
||||
});
|
|
@ -0,0 +1,42 @@
|
|||
requirejs.config({
|
||||
baseUrl: '/src/js',
|
||||
|
||||
map: {
|
||||
'*': {
|
||||
'underscore': 'lodash',
|
||||
'canvas_packages': '../../vendor/packages',
|
||||
}
|
||||
},
|
||||
|
||||
paths: {
|
||||
'text': '../../vendor/js/require/text',
|
||||
'i18n': '../../vendor/js/require/i18n',
|
||||
'jsx': '../../vendor/js/require/jsx',
|
||||
'JSXTransformer': '../../vendor/js/require/JSXTransformer-0.11.0.min',
|
||||
|
||||
// ========================================================================
|
||||
// CQS dependencies
|
||||
'rsvp': '../../vendor/js/rsvp.min',
|
||||
// ========================================================================
|
||||
|
||||
// ========================================================================
|
||||
// Aliases to frequently-used Canvas packages
|
||||
'react': '../../vendor/packages/react',
|
||||
'lodash': '../../vendor/packages/lodash',
|
||||
'd3': '../../vendor/packages/d3',
|
||||
// ========================================================================
|
||||
|
||||
// ========================================================================
|
||||
// Internal, for package providers only:
|
||||
'canvas': '../../vendor/canvas/public/javascripts',
|
||||
},
|
||||
|
||||
shim: {
|
||||
},
|
||||
|
||||
jsx: {
|
||||
fileExtension: '.jsx'
|
||||
},
|
||||
});
|
||||
|
||||
require([ 'boot' ]);
|
|
@ -0,0 +1,54 @@
|
|||
define(function(require) {
|
||||
var React = require('react');
|
||||
|
||||
var ChartMixin = {
|
||||
defaults: {
|
||||
updateChart: function(props) {
|
||||
this.removeChart();
|
||||
this.createChart(this.getDOMNode(), props);
|
||||
},
|
||||
|
||||
removeChart: function() {
|
||||
if (this.__svg) {
|
||||
this.__svg.remove();
|
||||
delete this.__svg;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
mixin: {
|
||||
componentWillMount: function() {
|
||||
if (typeof this.createChart !== 'function') {
|
||||
throw "ChartMixin: you must define a createChart() method that returns a d3 element";
|
||||
}
|
||||
|
||||
if (!this.updateChart) {
|
||||
this.updateChart = ChartMixin.defaults.updateChart;
|
||||
}
|
||||
|
||||
if (!this.removeChart) {
|
||||
this.removeChart = ChartMixin.defaults.removeChart;
|
||||
}
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
this.__svg = this.createChart(this.getDOMNode(), this.props);
|
||||
},
|
||||
|
||||
shouldComponentUpdate: function(nextProps/*, nextState*/) {
|
||||
this.updateChart(nextProps);
|
||||
return false;
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
this.removeChart();
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return React.DOM.svg({ className: "chart" });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return ChartMixin;
|
||||
});
|
|
@ -0,0 +1,109 @@
|
|||
define(function(require) {
|
||||
var Dispatcher = require('../../core/dispatcher');
|
||||
|
||||
var ActorMixin = {
|
||||
getInitialState: function() {
|
||||
return {
|
||||
actionIndex: null
|
||||
};
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
storeError: null
|
||||
};
|
||||
},
|
||||
|
||||
componentWillReceiveProps: function(nextProps) {
|
||||
var storeError = nextProps.storeError;
|
||||
|
||||
if (storeError && storeError.actionIndex === this.state.actionIndex) {
|
||||
this.setState({ storeError: storeError });
|
||||
}
|
||||
},
|
||||
|
||||
componentDidUpdate: function() {
|
||||
if (this.state.storeError) {
|
||||
if (this.onStoreError) {
|
||||
this.onStoreError(this.state.storeError);
|
||||
}
|
||||
|
||||
// Consume it so that the handling code doesn't get called repeatedly.
|
||||
this.setState({ storeError: null });
|
||||
}
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
this.lastAction = undefined;
|
||||
},
|
||||
|
||||
/**
|
||||
* Convenient method for consuming events.
|
||||
*
|
||||
* @param {Event} e
|
||||
* Something that responds to #preventDefault().
|
||||
*/
|
||||
consume: function(e) {
|
||||
if (e) {
|
||||
e.preventDefault();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Send an action via the Dispatcher, track the action promise, and any
|
||||
* error the handler raises.
|
||||
*
|
||||
* A reference to the action handler's promise will be kept in
|
||||
* `this.lastAction`. The index of the action is tracked in
|
||||
* this.state.actionIndex.
|
||||
*
|
||||
* If an error is raised, it will be accessible in `this.state.storeError`.
|
||||
*
|
||||
* @param {String} action (required)
|
||||
* Unique action identifier. Must be scoped by the store key, e.g:
|
||||
* "categories:save", or "users:changePassword".
|
||||
*
|
||||
* @param {Object} [params={}]
|
||||
* Action payload.
|
||||
*
|
||||
* @param {Object} [options={}]
|
||||
* @param {Boolean} [options.track=true]
|
||||
* Pass as false if you don't want the mixin to perform any tracking.
|
||||
*
|
||||
* @return {RSVP.Promise}
|
||||
* The action promise which will fulfill if the action succeeds,
|
||||
* or fail if the action doesn't. Failure will be presented by
|
||||
* an error that adheres to the UIError interface.
|
||||
*/
|
||||
sendAction: function(action, params, options) {
|
||||
var service;
|
||||
var setState;
|
||||
|
||||
service = Dispatcher.dispatch(action, params);
|
||||
|
||||
if (options && options.track === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
setState = this.setState.bind(this);
|
||||
this.lastAction = service.promise;
|
||||
|
||||
setState({
|
||||
actionIndex: service.index
|
||||
});
|
||||
|
||||
service.promise.then(null, function(error) {
|
||||
setState({
|
||||
storeError: {
|
||||
actionIndex: service.index,
|
||||
error: error
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return service.promise;
|
||||
}
|
||||
};
|
||||
|
||||
return ActorMixin;
|
||||
});
|
|
@ -0,0 +1,17 @@
|
|||
define(function(require) {
|
||||
var convertCase = require('../../util/convert_case');
|
||||
var _ = require('lodash');
|
||||
var pick = _.pick;
|
||||
var camelize = convertCase.camelize;
|
||||
|
||||
/**
|
||||
* Pick certain keys out of an object, and converts them to camelCase.
|
||||
*
|
||||
* @param {Object} set
|
||||
* @param {String[]} keys
|
||||
* @return {Object}
|
||||
*/
|
||||
return function pickAndNormalize(set, keys) {
|
||||
return camelize(pick(set || {}, keys));
|
||||
};
|
||||
});
|
|
@ -0,0 +1,16 @@
|
|||
define(function(require) {
|
||||
var Backbone = require('canvas_packages/backbone');
|
||||
var pickAndNormalize = require('./common/pick_and_normalize');
|
||||
var K = require('../constants');
|
||||
|
||||
return Backbone.Model.extend({
|
||||
parse: function(payload) {
|
||||
var attrs = pickAndNormalize(payload, K.QUIZ_REPORT_ATTRS);
|
||||
|
||||
attrs.progress = pickAndNormalize(payload.progress, K.PROGRESS_ATTRS);
|
||||
attrs.file = pickAndNormalize(payload.file, K.ATTACHMENT_ATTRS);
|
||||
|
||||
return attrs;
|
||||
}
|
||||
});
|
||||
});
|
|
@ -0,0 +1,44 @@
|
|||
define(function(require) {
|
||||
var Backbone = require('canvas_packages/backbone');
|
||||
var pickAndNormalize = require('./common/pick_and_normalize');
|
||||
var K = require('../constants');
|
||||
var _ = require('lodash');
|
||||
var wrap = require('../util/array_wrap');
|
||||
var findWhere = _.findWhere;
|
||||
|
||||
var QuizStatistics = Backbone.Model.extend({
|
||||
parse: function(payload) {
|
||||
var attrs = {};
|
||||
|
||||
attrs = pickAndNormalize(payload, K.QUIZ_STATISTICS_ATTRS);
|
||||
|
||||
attrs.submissionStatistics = pickAndNormalize(
|
||||
payload.submission_statistics,
|
||||
K.SUBMISSION_STATISTICS_ATTRS
|
||||
);
|
||||
|
||||
attrs.questionStatistics = wrap(payload.question_statistics).map(function(questionStatistics) {
|
||||
var attrs = pickAndNormalize(
|
||||
questionStatistics,
|
||||
K.QUESTION_STATISTICS_ATTRS
|
||||
);
|
||||
|
||||
if (attrs.pointBiserials) {
|
||||
attrs.pointBiserials = attrs.pointBiserials.map(function(pointBiserial) {
|
||||
return pickAndNormalize(pointBiserial, K.POINT_BISERIAL_ATTRS);
|
||||
});
|
||||
|
||||
attrs.discriminationIndex = findWhere(attrs.pointBiserials, {
|
||||
correct: true
|
||||
}).pointBiserial;
|
||||
}
|
||||
|
||||
return attrs;
|
||||
});
|
||||
|
||||
return attrs;
|
||||
}
|
||||
});
|
||||
|
||||
return QuizStatistics;
|
||||
});
|
|
@ -0,0 +1,100 @@
|
|||
define(function(require) {
|
||||
var _ = require('lodash');
|
||||
var extend = _.extend;
|
||||
|
||||
var MULTIPLE_ANSWERS = 'multiple_answers_question';
|
||||
|
||||
// internal
|
||||
var isMultipleAnswers = function(questionType) {
|
||||
return questionType === MULTIPLE_ANSWERS;
|
||||
};
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* @hide
|
||||
*
|
||||
* Calculates a similar ratio to #ratio but for questions that require a
|
||||
* student to choose more than one answer for their response to be considered
|
||||
* correct. As such, a "partially" correct response does not count towards
|
||||
* the correct response ratio.
|
||||
*/
|
||||
var ratioForMultipleAnswers = function() {
|
||||
return this.correct / this.participantCount;
|
||||
};
|
||||
|
||||
/**
|
||||
* @class RatioCalculator
|
||||
*
|
||||
* A utility class for calculating response ratios for a given question
|
||||
* statistics object.
|
||||
*
|
||||
* The ratio calculation may differ based on the question type, this class
|
||||
* takes care of it by exposing a single API #ratio() that hides those details
|
||||
* from you.
|
||||
*/
|
||||
var RatioCalculator = function(questionType, options) {
|
||||
this.questionType = questionType;
|
||||
|
||||
if (options) {
|
||||
this.answerPool = options.answerPool;
|
||||
this.participantCount = options.participantCount;
|
||||
}
|
||||
|
||||
return this;
|
||||
};
|
||||
|
||||
extend(RatioCalculator.prototype, {
|
||||
participantCount: 0,
|
||||
|
||||
setParticipantCount: function(count) {
|
||||
this.participantCount = count;
|
||||
},
|
||||
|
||||
/**
|
||||
* @property {Object[]} answerPool
|
||||
* This is the set of answers that we'll use to calculate the ratio.
|
||||
*
|
||||
* Synopsis of the expected answer objects in the set:
|
||||
*
|
||||
* {
|
||||
* "responses": 0,
|
||||
* "correct": true
|
||||
* }
|
||||
*
|
||||
* Most question types will have these defined in the top-level "answers" set,
|
||||
* but for some others that support answer sets, these could be found in
|
||||
* `answer_sets.@each.answer_matches`.
|
||||
*/
|
||||
answerPool: [],
|
||||
|
||||
setAnswerPool: function(pool) {
|
||||
this.answerPool = pool;
|
||||
},
|
||||
|
||||
/**
|
||||
* Calculates the ratio of students who answered this question correctly
|
||||
* (partially correct answers do not count when applicable)
|
||||
*
|
||||
* @return {Number} A scalar, the ratio.
|
||||
*/
|
||||
getRatio: function() {
|
||||
var participantCount = this.participantCount || 0;
|
||||
var correctResponseCount;
|
||||
|
||||
if (participantCount <= 0) {
|
||||
return 0;
|
||||
}
|
||||
else if (isMultipleAnswers(this.questionType)) {
|
||||
return ratioForMultipleAnswers.call(this);
|
||||
}
|
||||
|
||||
correctResponseCount = this.answerPool.reduce(function(sum, answer) {
|
||||
return (answer.correct) ? sum + answer.responses : sum;
|
||||
}, 0);
|
||||
|
||||
return parseFloat(correctResponseCount) / participantCount;
|
||||
}
|
||||
});
|
||||
|
||||
return RatioCalculator;
|
||||
});
|
|
@ -0,0 +1,99 @@
|
|||
define(function(require) {
|
||||
var Store = require('../core/store');
|
||||
var Adapter = require('../core/adapter');
|
||||
var config = require('../config');
|
||||
var K = require('../constants');
|
||||
var RSVP = require('rsvp');
|
||||
var QuizReports = require('../collections/quiz_reports');
|
||||
var QuizStats = require('../collections/quiz_statistics');
|
||||
var onError = config.onError;
|
||||
var quizReports = new QuizReports();
|
||||
var quizStats = new QuizStats([]);
|
||||
|
||||
var store = new Store('statistics', {
|
||||
/**
|
||||
* Load quiz statistics and reports.
|
||||
* Requires config.quizStatisticsUrl to be set.
|
||||
*
|
||||
* @async
|
||||
* @emit change
|
||||
*/
|
||||
load: function() {
|
||||
var stats, reports;
|
||||
|
||||
if (!config.quizStatisticsUrl) {
|
||||
return onError('Missing configuration parameter "quizStatisticsUrl".');
|
||||
}
|
||||
else if (!config.quizReportsUrl) {
|
||||
return onError('Missing configuration parameter "quizReportsUrl".');
|
||||
}
|
||||
|
||||
stats = Adapter.request({
|
||||
type: 'GET',
|
||||
url: config.quizStatisticsUrl
|
||||
}).then(function(quizStatisticsPayload) {
|
||||
quizStats.reset(quizStatisticsPayload, { parse: true });
|
||||
});
|
||||
|
||||
reports = Adapter.request({
|
||||
type: 'GET',
|
||||
url: config.quizReportsUrl
|
||||
}).then(function(quizReportsPayload) {
|
||||
quizReports.add(quizReportsPayload, { parse: true });
|
||||
});
|
||||
|
||||
return RSVP.all([ stats, reports ]).then(function() {
|
||||
store.emitChange();
|
||||
});
|
||||
},
|
||||
|
||||
getQuizStatistics: function() {
|
||||
if (quizStats.length) {
|
||||
return quizStats.first().toJSON();
|
||||
}
|
||||
},
|
||||
|
||||
getSubmissionStatistics: function() {
|
||||
if (quizStats.length) {
|
||||
return quizStats.first().get('submissionStatistics');
|
||||
}
|
||||
},
|
||||
|
||||
getQuestionStatistics: function() {
|
||||
if (quizStats.length) {
|
||||
return quizStats.first().get('questionStatistics');
|
||||
}
|
||||
},
|
||||
|
||||
getQuizReports: function() {
|
||||
return quizReports.toJSON();
|
||||
},
|
||||
|
||||
actions: {
|
||||
generateReport: function(reportType, onChange, onError) {
|
||||
Adapter.request({
|
||||
type: 'POST',
|
||||
url: config.quizReportsUrl,
|
||||
data: {
|
||||
quiz_reports: [{
|
||||
report_type: reportType,
|
||||
includes_all_versions: true
|
||||
}],
|
||||
include: ['progress', 'file']
|
||||
}
|
||||
}).then(function(quizReportsPayload) {
|
||||
quizReports.add(quizReportsPayload, { parse: true });
|
||||
onChange();
|
||||
}, onError);
|
||||
}
|
||||
},
|
||||
|
||||
__reset__: function() {
|
||||
quizStats.reset();
|
||||
quizReports.reset();
|
||||
return Store.prototype.__reset__.call(this);
|
||||
}
|
||||
});
|
||||
|
||||
return store;
|
||||
});
|
|
@ -0,0 +1,9 @@
|
|||
define(function() {
|
||||
return function wrap(value) {
|
||||
return Array.isArray(value) ?
|
||||
value :
|
||||
value === undefined ?
|
||||
[] :
|
||||
[ value ];
|
||||
};
|
||||
});
|
|
@ -0,0 +1,24 @@
|
|||
define(function(require) {
|
||||
var React = require('react');
|
||||
/**
|
||||
* Shim for React.addons.classSet.
|
||||
*
|
||||
* @param {Object} set
|
||||
* A set of class strings and booleans. If the boolean is truthy,
|
||||
* the class will be appended to the className.
|
||||
*
|
||||
* @return {String}
|
||||
* The produced class string ready for use as a className prop.
|
||||
*/
|
||||
var classSet = function(set) {
|
||||
return Object.keys(set).reduce(function(classes, key) {
|
||||
if (!!set[key]) {
|
||||
classes.push(key);
|
||||
}
|
||||
|
||||
return classes;
|
||||
}, []).join(' ');
|
||||
};
|
||||
|
||||
return (React.addons || {}).classSet || classSet;
|
||||
});
|
|
@ -0,0 +1,34 @@
|
|||
define(function(require) {
|
||||
var Inflections = require('./inflections');
|
||||
var camelizeStr = Inflections.camelize;
|
||||
var underscoreStr = Inflections.underscore;
|
||||
|
||||
return {
|
||||
// Convert all property keys in an object to camelCase
|
||||
camelize: function(props) {
|
||||
var prop;
|
||||
var attrs = {};
|
||||
|
||||
for (prop in props) {
|
||||
if (props.hasOwnProperty(prop)) {
|
||||
attrs[camelizeStr(prop, true)] = props[prop];
|
||||
}
|
||||
}
|
||||
|
||||
return attrs;
|
||||
},
|
||||
|
||||
underscore: function(props) {
|
||||
var prop;
|
||||
var attrs = {};
|
||||
|
||||
for (prop in props) {
|
||||
if (props.hasOwnProperty(prop)) {
|
||||
attrs[underscoreStr(prop)] = props[prop];
|
||||
}
|
||||
}
|
||||
|
||||
return attrs;
|
||||
}
|
||||
};
|
||||
});
|
|
@ -0,0 +1,30 @@
|
|||
define(function() {
|
||||
var INTERPOLATER = /\%\{([^\}]+)\}/g;
|
||||
|
||||
/**
|
||||
* Stupid i18n interpolator that interpolates anything between %{} with
|
||||
* a value you pass in @options.
|
||||
*
|
||||
* @param {String} contents
|
||||
* The i18n text block you're interpolating.
|
||||
*
|
||||
* @param {Object} options
|
||||
* Pairs of variable names and their interpolation values.
|
||||
* The variable names should be snake_cased.
|
||||
*
|
||||
* @return {String}
|
||||
* The interpolated text.
|
||||
*/
|
||||
return function i18nInterpolate(contents, options) {
|
||||
var variables = contents.match(INTERPOLATER);
|
||||
|
||||
if (variables) {
|
||||
variables.forEach(function(variable) {
|
||||
var optionKey = variable.substr(2, variable.length - 3);
|
||||
contents = contents.replace(new RegExp(variable, 'g'), options[optionKey]);
|
||||
});
|
||||
}
|
||||
|
||||
return contents;
|
||||
};
|
||||
});
|
|
@ -0,0 +1,20 @@
|
|||
define(function() {
|
||||
return {
|
||||
camelize: function(str, lowerFirst) {
|
||||
return (str || '').replace (/(?:^|[-_])(\w)/g, function (_, c, index) {
|
||||
if (index === 0 && lowerFirst) {
|
||||
return c ? c.toLowerCase() : '';
|
||||
}
|
||||
else {
|
||||
return c ? c.toUpperCase () : '';
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
underscore: function(str) {
|
||||
return str.replace(/([A-Z])/g, function($1){
|
||||
return '_' + $1.toLowerCase();
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
|
@ -0,0 +1,32 @@
|
|||
define([ '../config' ], function(config) {
|
||||
/**
|
||||
* @member Util
|
||||
* @method round
|
||||
* Round a number to N digits.
|
||||
*
|
||||
* TODO: import as a Canvas package (we have it in util/round.coffee)
|
||||
*
|
||||
* @param {Number|String} n
|
||||
* Your number
|
||||
*
|
||||
* @param {Number} [digits=config.precision]
|
||||
* Precision of the returned float (number of digits after the
|
||||
* decimal point.)
|
||||
*
|
||||
* @return {Float}
|
||||
* The rounded number, ready for human-consumption.
|
||||
*/
|
||||
return function round(n, precision) {
|
||||
var scale;
|
||||
|
||||
if (precision === undefined) {
|
||||
precision = config.precision;
|
||||
}
|
||||
|
||||
if (typeof n !== 'number' || !(n instanceof Number)) {
|
||||
n = parseFloat(n);
|
||||
}
|
||||
|
||||
return n.toFixed(precision);
|
||||
}
|
||||
});
|
|
@ -0,0 +1,39 @@
|
|||
define(function(require) {
|
||||
var floor = Math.floor;
|
||||
|
||||
var pad = function(duration) {
|
||||
return ('00' + duration).slice(-2);
|
||||
};
|
||||
|
||||
/**
|
||||
* @member Util
|
||||
*
|
||||
* Format a duration given in seconds into a stopwatch-style timer, e.g:
|
||||
*
|
||||
* - 1 second => `00:01`
|
||||
* - 30 seconds => `00:30`
|
||||
* - 84 seconds => `01:24`
|
||||
* - 7230 seconds => `02:00:30`
|
||||
* - 7530 seconds => `02:05:30`
|
||||
*
|
||||
* @param {Number} seconds
|
||||
* The duration in seconds.
|
||||
*
|
||||
* @return {String}
|
||||
*/
|
||||
var secondsToTime = function(seconds) {
|
||||
var hh, mm, ss;
|
||||
|
||||
if (seconds > 3600) {
|
||||
hh = floor(seconds / 3600);
|
||||
mm = floor((seconds - hh*3600) / 60);
|
||||
ss = seconds % 60;
|
||||
return [ hh, mm, ss ].map(pad).join(':');
|
||||
}
|
||||
else {
|
||||
return [ seconds / 60, seconds % 60 ].map(floor).map(pad).join(':');
|
||||
}
|
||||
};
|
||||
|
||||
return secondsToTime;
|
||||
});
|
|
@ -0,0 +1,59 @@
|
|||
define(function(require) {
|
||||
var RSVP = require('rsvp');
|
||||
var successCodes = [ 200, 204 ];
|
||||
|
||||
var parse = function(xhr) {
|
||||
var payload;
|
||||
|
||||
if (xhr.responseJSON) {
|
||||
return xhr.responseJSON;
|
||||
}
|
||||
else if ((xhr.responseText || '').length) {
|
||||
payload = (xhr.responseText || '').replace('while(1);', '');
|
||||
|
||||
try {
|
||||
payload = JSON.parse(payload);
|
||||
} catch(e) {
|
||||
payload = xhr.responseText;
|
||||
}
|
||||
}
|
||||
else {
|
||||
payload = undefined;
|
||||
}
|
||||
|
||||
return payload;
|
||||
};
|
||||
|
||||
return function xhrRequest(options) {
|
||||
var url = options.url;
|
||||
var method = options.type || 'GET';
|
||||
var async = options.async === undefined ? true : !!options.async;
|
||||
var data = options.data;
|
||||
|
||||
return new RSVP.Promise(function(resolve, reject) {
|
||||
var xhr = new XMLHttpRequest();
|
||||
|
||||
xhr.onreadystatechange = function() {
|
||||
// all is well
|
||||
if (xhr.readyState === 4) {
|
||||
if (successCodes.indexOf(xhr.status) > -1) {
|
||||
resolve(parse(xhr), xhr.status, xhr);
|
||||
}
|
||||
else {
|
||||
reject(parse(xhr), xhr.status, xhr);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
xhr.open(method, url, async);
|
||||
|
||||
if (options.headers) {
|
||||
Object.keys(options.headers).forEach(function(header) {
|
||||
xhr.setRequestHeader(header, options.headers[header]);
|
||||
});
|
||||
}
|
||||
|
||||
xhr.send(JSON.stringify(data));
|
||||
});
|
||||
};
|
||||
});
|
|
@ -0,0 +1,3 @@
|
|||
define(function() {
|
||||
return '1.0.0';
|
||||
});
|
|
@ -0,0 +1,76 @@
|
|||
/** @jsx React.DOM */
|
||||
define(function(require) {
|
||||
var React = require('react');
|
||||
var Summary = require('jsx!./summary');
|
||||
var I18n = require('i18n!quiz_statistics');
|
||||
var _ = require('lodash');
|
||||
var QuestionRenderer = require('jsx!./question');
|
||||
var MultipleChoiceRenderer = require('jsx!./questions/multiple_choice');
|
||||
|
||||
var extend = _.extend;
|
||||
var Renderers = {
|
||||
'multiple_choice_question': MultipleChoiceRenderer,
|
||||
'true_false_question': MultipleChoiceRenderer,
|
||||
};
|
||||
|
||||
var Statistics = React.createClass({
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
quizStatistics: {
|
||||
submissionStatistics: {},
|
||||
questionStatistics: [],
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var props = this.props;
|
||||
var quizStatistics = this.props.quizStatistics;
|
||||
var submissionStatistics = quizStatistics.submissionStatistics;
|
||||
var questionStatistics = quizStatistics.questionStatistics;
|
||||
var participantCount = submissionStatistics.uniqueCount;
|
||||
|
||||
return(
|
||||
<div id="canvas-quiz-statistics">
|
||||
<section>
|
||||
<Summary
|
||||
pointsPossible={quizStatistics.pointsPossible}
|
||||
scoreAverage={submissionStatistics.scoreAverage}
|
||||
scoreHigh={submissionStatistics.scoreHigh}
|
||||
scoreLow={submissionStatistics.scoreLow}
|
||||
scoreStdev={submissionStatistics.scoreStdev}
|
||||
durationAverage={submissionStatistics.durationAverage}
|
||||
quizReports={this.props.quizReports}
|
||||
scores={submissionStatistics.scores}
|
||||
/>
|
||||
</section>
|
||||
|
||||
<section id="question-statistics-section">
|
||||
<header className="padded">
|
||||
<h3 className="section-title inline">
|
||||
{I18n.t('question_breakdown', 'Question Breakdown')}
|
||||
</h3>
|
||||
|
||||
<aside className="pull-right">
|
||||
</aside>
|
||||
</header>
|
||||
|
||||
{questionStatistics.map(this.renderQuestion.bind(null, participantCount))}
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
||||
renderQuestion: function(participantCount, question) {
|
||||
var renderer = Renderers[question.questionType] || QuestionRenderer;
|
||||
var questionProps = extend({}, question, {
|
||||
key: 'question-' + question.id,
|
||||
participantCount: participantCount
|
||||
});
|
||||
|
||||
return renderer(questionProps);
|
||||
}
|
||||
});
|
||||
|
||||
return Statistics;
|
||||
});
|
|
@ -0,0 +1,196 @@
|
|||
/** @jsx React.DOM */
|
||||
define(function(require) {
|
||||
var React = require('react');
|
||||
var d3 = require('d3');
|
||||
var _ = require('lodash');
|
||||
var ChartMixin = require('../../mixins/chart');
|
||||
|
||||
var mapBy = _.map;
|
||||
var findWhere = _.findWhere;
|
||||
var compact = _.compact;
|
||||
|
||||
var Chart = React.createClass({
|
||||
mixins: [ ChartMixin.mixin ],
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
answers: [],
|
||||
|
||||
/**
|
||||
* @property {Number} [barWidth=30]
|
||||
* Width of the bars in the chart in pixels.
|
||||
*/
|
||||
barWidth: 30,
|
||||
|
||||
/**
|
||||
* @property {Number} [barMargin=1]
|
||||
*
|
||||
* Whitespace to offset the bars by, in pixels.
|
||||
*/
|
||||
barMargin: 1,
|
||||
xOffset: 16,
|
||||
yAxisLabel: '',
|
||||
xAxisLabels: false,
|
||||
linearScale: true,
|
||||
width: 'auto',
|
||||
height: 120
|
||||
|
||||
};
|
||||
},
|
||||
|
||||
createChart: function(node, props) {
|
||||
var otherAnswers;
|
||||
var data = props.answers;
|
||||
var container = this.getDOMNode();
|
||||
|
||||
var sz = data.reduce(function(sum, item) {
|
||||
return sum + item.y;
|
||||
}, 0);
|
||||
|
||||
var highest = d3.max(mapBy(data, 'y'));
|
||||
|
||||
var width, height;
|
||||
var margin = { top: 0, right: 0, bottom: 0, left: 0 };
|
||||
|
||||
if (props.width === 'auto') {
|
||||
width = container.offsetWidth;
|
||||
}
|
||||
else {
|
||||
width = parseInt(props.width, 10);
|
||||
}
|
||||
|
||||
width -= margin.left - margin.right;
|
||||
height = props.height - margin.top - margin.bottom;
|
||||
|
||||
var barWidth = props.barWidth;
|
||||
var barMargin = props.barMargin;
|
||||
var xOffset = props.xOffset;
|
||||
|
||||
var x = d3.scale.ordinal()
|
||||
.rangeRoundBands([0, barWidth * sz], 0.025);
|
||||
|
||||
var y = d3.scale.linear()
|
||||
.range([height, 0]);
|
||||
|
||||
var visibilityThreshold = Math.max(5, y(highest) / 100.0);
|
||||
|
||||
var svg = d3.select(node)
|
||||
.attr('width', width + margin.left + margin.right)
|
||||
.attr('height', height + margin.top + margin.bottom)
|
||||
.append('g')
|
||||
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
|
||||
|
||||
var classifyChartBar = this.classifyChartBar;
|
||||
|
||||
x.domain(data.map(function(d, i) { return d.label || i; }));
|
||||
y.domain([ 0, sz ]);
|
||||
|
||||
svg.selectAll('.bar')
|
||||
.data(data)
|
||||
.enter().append('rect')
|
||||
.attr("class", function(d) {
|
||||
return classifyChartBar(d);
|
||||
})
|
||||
.attr("x", function(d, i) {
|
||||
return i * (barWidth + barMargin) + xOffset;
|
||||
})
|
||||
.attr("width", barWidth)
|
||||
.attr("y", function(d) {
|
||||
return y(d.y) - visibilityThreshold;
|
||||
})
|
||||
.attr("height", function(d) {
|
||||
return height - y(d.y) + visibilityThreshold;
|
||||
});
|
||||
|
||||
// If the special "No Answer" is present, we represent it as a diagonally-
|
||||
// striped bar, but to do that we need to render the <svg:pattern> that
|
||||
// generates the stripes and use that as a fill pattern, and we also need
|
||||
// to create the <svg:rect> that will be filled with that pattern.
|
||||
otherAnswers = compact([
|
||||
findWhere(data, { id: 'other' }),
|
||||
findWhere(data, { id: 'none' })
|
||||
]);
|
||||
|
||||
if (otherAnswers.length) {
|
||||
this.renderStripePattern(svg);
|
||||
svg.selectAll('.bar.bar-striped')
|
||||
.data(otherAnswers)
|
||||
.enter().append('rect')
|
||||
.attr('class', 'bar bar-striped')
|
||||
// We need to inline the fill style because we are referencing an
|
||||
// inline pattern (#diagonalStripes) which is unreachable from a CSS
|
||||
// directive.
|
||||
//
|
||||
// See this link [StackOverflow] for more info: http://bit.ly/1uDTqyn
|
||||
.attr('style', 'fill: url(#diagonalStripes);')
|
||||
// remove 2 pixels from width and height, and offset it by {1,1} on
|
||||
// both axes to "contain" it inside the margins of the bg rect
|
||||
.attr('x', function(d) {
|
||||
return data.indexOf(d) * (barWidth + barMargin) + xOffset + 1;
|
||||
})
|
||||
.attr('width', barWidth-2)
|
||||
.attr('y', function(d) {
|
||||
return y(d.y + visibilityThreshold) + 1;
|
||||
})
|
||||
.attr('height', function(d) {
|
||||
return height - y(d.y + visibilityThreshold) - 2;
|
||||
});
|
||||
}
|
||||
|
||||
return svg;
|
||||
},
|
||||
|
||||
renderStripePattern: function(svg) {
|
||||
svg.append('pattern')
|
||||
.attr('id', 'diagonalStripes')
|
||||
.attr('width', 5)
|
||||
.attr('height', 5)
|
||||
.attr('patternTransform', 'rotate(45 0 0)')
|
||||
.attr('patternUnits', 'userSpaceOnUse')
|
||||
.append('g')
|
||||
.append('path')
|
||||
.attr('d', 'M0,0 L0,10');
|
||||
},
|
||||
|
||||
classifyChartBar: function(answer) {
|
||||
if (answer.correct) {
|
||||
return 'bar bar-highlighted';
|
||||
} else {
|
||||
return 'bar';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
var CorrectAnswerDonut = React.createClass({
|
||||
propTypes: {
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
answers: [],
|
||||
children: []
|
||||
};
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var chartData = this.props.answers.map(function(answer) {
|
||||
return {
|
||||
id: ''+answer.id,
|
||||
y: answer.responses,
|
||||
correct: answer.correct
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Chart answers={chartData} />
|
||||
|
||||
<div className="auxiliary">
|
||||
{this.props.children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return CorrectAnswerDonut;
|
||||
});
|
|
@ -0,0 +1,82 @@
|
|||
/** @jsx React.DOM */
|
||||
define(function(require) {
|
||||
var React = require('react');
|
||||
var d3 = require('d3');
|
||||
var ChartMixin = require('../../mixins/chart');
|
||||
|
||||
var CIRCLE = 2 * Math.PI;
|
||||
var FMT_PERCENT = d3.format('%');
|
||||
|
||||
var Chart = React.createClass({
|
||||
mixins: [ ChartMixin.mixin ],
|
||||
|
||||
createChart: function(node, props) {
|
||||
var ratio = props.correctResponseRatio;
|
||||
var diameter = props.diameter;
|
||||
var radius = diameter / 2;
|
||||
|
||||
var arc = d3.svg.arc()
|
||||
.innerRadius(radius)
|
||||
.outerRadius(diameter / 2.5)
|
||||
.startAngle(0);
|
||||
|
||||
var svg = d3.select(node)
|
||||
.attr('width', radius)
|
||||
.attr('height', radius)
|
||||
.append('g')
|
||||
.attr('transform', 'translate(' + radius + ',' + radius + ')');
|
||||
|
||||
// background circle that's always "empty" (shaded in light color)
|
||||
svg.append('path')
|
||||
.datum({ endAngle: CIRCLE })
|
||||
.attr('class', 'background')
|
||||
.attr('d', arc);
|
||||
|
||||
// foreground circle that fills up based on ratio (green, or flashy)
|
||||
svg.append('path')
|
||||
.datum({ endAngle: CIRCLE * ratio })
|
||||
.attr('class', 'foreground')
|
||||
.attr('d', arc);
|
||||
|
||||
// text inside the circle
|
||||
svg.append('text')
|
||||
.attr('text-anchor', 'middle')
|
||||
.attr('dy', '.35em')
|
||||
.text(FMT_PERCENT(ratio));
|
||||
|
||||
return svg;
|
||||
},
|
||||
});
|
||||
|
||||
var CorrectAnswerDonut = React.createClass({
|
||||
propTypes: {
|
||||
correctResponseRatio: React.PropTypes.number.isRequired
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
/**
|
||||
* @config {Number} [radius=80]
|
||||
* Diameter of the donut chart in pixels.
|
||||
*/
|
||||
diameter: 80,
|
||||
correctResponseRatio: 0,
|
||||
children: []
|
||||
};
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return (
|
||||
<div>
|
||||
{this.transferPropsTo(Chart())}
|
||||
|
||||
<div className="auxiliary">
|
||||
{this.props.children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return CorrectAnswerDonut;
|
||||
});
|
|
@ -0,0 +1,159 @@
|
|||
/** @jsx React.DOM */
|
||||
define(function(require) {
|
||||
var React = require('react');
|
||||
var d3 = require('d3');
|
||||
var K = require('../../constants');
|
||||
var I18n = require('i18n!quiz_statistics');
|
||||
var classSet = require('../../util/class_set');
|
||||
var ChartMixin = require('../../mixins/chart');
|
||||
var Dialog = require('jsx!../../components/dialog');
|
||||
var Help = require('jsx!./discrimination_index/help');
|
||||
|
||||
var divide = function(x, y) {
|
||||
return (parseFloat(x) / y) || 0;
|
||||
};
|
||||
|
||||
var Chart = React.createClass({
|
||||
mixins: [ ChartMixin.mixin ],
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
correct: [],
|
||||
total: [],
|
||||
ratio: []
|
||||
};
|
||||
},
|
||||
|
||||
createChart: function(node, props) {
|
||||
var barHeight, barWidth, svg;
|
||||
|
||||
barHeight = props.height / 3;
|
||||
barWidth = props.width / 2;
|
||||
|
||||
svg = d3.select(node)
|
||||
.attr('width', props.width)
|
||||
.attr('height', props.height)
|
||||
.append('g');
|
||||
|
||||
svg.selectAll('.bar.correct')
|
||||
.data(props.ratio)
|
||||
.enter()
|
||||
.append('rect')
|
||||
.attr('class', 'bar correct')
|
||||
.attr('x', barWidth)
|
||||
.attr('width', function(correctRatio) {
|
||||
return correctRatio * barWidth;
|
||||
}).attr('y', function(d, bracket) {
|
||||
return bracket * barHeight;
|
||||
}).attr('height', function() {
|
||||
return barHeight - 1;
|
||||
});
|
||||
|
||||
svg.selectAll('.bar.incorrect')
|
||||
.data(props.ratio)
|
||||
.enter()
|
||||
.append('rect')
|
||||
.attr('class', 'bar incorrect')
|
||||
.attr('x', function(correctRatio) {
|
||||
return -1 * (1 - correctRatio * barWidth);
|
||||
}).attr('width', function(correctRatio) {
|
||||
return (1 - correctRatio) * barWidth;
|
||||
}).attr('y', function(d, bracket) {
|
||||
return bracket * barHeight;
|
||||
}).attr('height', function() {
|
||||
return barHeight - 1;
|
||||
});
|
||||
|
||||
this.__svg = svg;
|
||||
|
||||
return svg;
|
||||
},
|
||||
});
|
||||
|
||||
var DiscriminationIndex = React.createClass({
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
width: 270,
|
||||
height: 14 * 3,
|
||||
discriminationIndex: 0,
|
||||
topStudentCount: 0,
|
||||
middleStudentCount: 0,
|
||||
bottomStudentCount: 0,
|
||||
correctTopStudentCount: 0,
|
||||
correctMiddleStudentCount: 0,
|
||||
correctBottomStudentCount: 0,
|
||||
};
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var di = this.props.discriminationIndex;
|
||||
var sign = di > K.DISCRIMINATION_INDEX_THRESHOLD ? '+' : '-';
|
||||
var className = {
|
||||
'index': true,
|
||||
'positive': sign === '+',
|
||||
'negative': sign !== '+'
|
||||
};
|
||||
|
||||
var chartData;
|
||||
var stats = {
|
||||
top: {
|
||||
correct: this.props.correctTopStudentCount,
|
||||
total: this.props.topStudentCount,
|
||||
},
|
||||
mid: {
|
||||
correct: this.props.correctMiddleStudentCount,
|
||||
total: this.props.middleStudentCount,
|
||||
},
|
||||
bot: {
|
||||
correct: this.props.correctBottomStudentCount,
|
||||
total: this.props.bottomStudentCount,
|
||||
}
|
||||
};
|
||||
|
||||
chartData = {
|
||||
correct: [
|
||||
stats.top.correct, stats.mid.correct, stats.bot.correct
|
||||
],
|
||||
|
||||
total: [
|
||||
stats.top.total, stats.mid.total, stats.bot.total
|
||||
],
|
||||
|
||||
ratio: [
|
||||
divide(stats.top.correct, stats.top.total),
|
||||
divide(stats.mid.correct, stats.mid.total),
|
||||
divide(stats.bot.correct, stats.bot.total)
|
||||
]
|
||||
};
|
||||
|
||||
chartData.width = this.props.width;
|
||||
chartData.height = this.props.height;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p>
|
||||
<em className={classSet(className)}>
|
||||
<span className="sign">{sign}</span>
|
||||
{Math.abs((this.props.discriminationIndex || 0).toFixed(2))}
|
||||
</em>
|
||||
|
||||
{' '}
|
||||
|
||||
<strong>
|
||||
{I18n.t('discrimination_index', 'Discrimination Index')}
|
||||
</strong>
|
||||
|
||||
<Dialog
|
||||
tagName="i"
|
||||
content={Help}
|
||||
className="chart-help-trigger icon-question" />
|
||||
</p>
|
||||
|
||||
{Chart(chartData)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return DiscriminationIndex;
|
||||
});
|
|
@ -0,0 +1,34 @@
|
|||
/** @jsx React.DOM */
|
||||
define(function(require) {
|
||||
var React = require('react');
|
||||
var K = require('../../../constants');
|
||||
var Text = require('jsx!../../../components/text');
|
||||
var I18n = require('i18n!quiz_statistics');
|
||||
|
||||
var Help = React.createClass({
|
||||
render: function() {
|
||||
return(
|
||||
<Text
|
||||
phrase="discrimination_index_help"
|
||||
articleUrl={K.DISCRIMINATION_INDEX_HELP_ARTICLE_URL}>
|
||||
<p>
|
||||
This metric provides a measure of how well a single question can tell the
|
||||
difference (or discriminate) between students who do well on an exam and
|
||||
those who do not.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
It divides students into three groups based on their score on the whole
|
||||
quiz and displays those groups by who answered the question correctly.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
More information is available <a href="%{article_url}" target="_blank">here</a>.
|
||||
</p>
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return Help;
|
||||
});
|
|
@ -0,0 +1,13 @@
|
|||
/** @jsx React.DOM */
|
||||
define(function(require) {
|
||||
var React = require('react');
|
||||
var Question = React.createClass({
|
||||
render: function() {
|
||||
return(
|
||||
<div className="question-statistics" children={this.props.children} />
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return Question;
|
||||
});
|
|
@ -0,0 +1,121 @@
|
|||
/** @jsx React.DOM */
|
||||
define(function(require) {
|
||||
var React = require('react');
|
||||
var I18n = require('i18n!quiz_statistics');
|
||||
var Question = require('jsx!../question');
|
||||
var CorrectAnswerDonut = require('jsx!../charts/correct_answer_donut');
|
||||
var AnswerBars = require('jsx!../charts/answer_bars');
|
||||
var DiscriminationIndex = require('jsx!../charts/discrimination_index');
|
||||
var RatioCalculator = require('../../models/ratio_calculator');
|
||||
var round = require('../../util/round');
|
||||
|
||||
var MultipleChoice = React.createClass({
|
||||
getInitialState: function() {
|
||||
return {
|
||||
participantCount: 0,
|
||||
showingDetails: false,
|
||||
correctResponseRatio: 0
|
||||
};
|
||||
},
|
||||
|
||||
isShowingDetails: function() {
|
||||
return this.props.showDetails === undefined ?
|
||||
this.state.showingDetails :
|
||||
this.props.showDetails;
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
var calculator = new RatioCalculator(this.props.questionType, {
|
||||
answerPool: this.props.answers,
|
||||
participantCount: this.props.participantCount
|
||||
});
|
||||
|
||||
this.setState({
|
||||
calculator: calculator,
|
||||
correctResponseRatio: calculator.getRatio()
|
||||
});
|
||||
},
|
||||
|
||||
componentWillReceiveProps: function(nextProps) {
|
||||
this.updateCalculator(nextProps);
|
||||
},
|
||||
|
||||
updateCalculator: function(props) {
|
||||
this.state.calculator.setAnswerPool(props.answers);
|
||||
this.state.calculator.setParticipantCount(props.participantCount);
|
||||
this.setState({
|
||||
correctResponseRatio: this.state.calculator.getRatio()
|
||||
});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var crr = this.state.correctResponseRatio;
|
||||
var attemptsLabel = I18n.t('attempts', 'Attempts: %{count} out of %{total}', {
|
||||
count: this.props.answeredStudentCount,
|
||||
total: this.props.participantCount
|
||||
});
|
||||
|
||||
var correctResponseRatioLabel = I18n.t('correct_response_ratio',
|
||||
'%{ratio}% of your students correctly answered this question.', {
|
||||
ratio: round(crr * 100.0, 0)
|
||||
});
|
||||
|
||||
return(
|
||||
<Question>
|
||||
<header>
|
||||
<span className="question-attempts">{attemptsLabel}</span>
|
||||
<aside className="pull-right">
|
||||
<button onClick={this.toggleDetails} className="btn">
|
||||
{this.isShowingDetails() ?
|
||||
<i className="icon-collapse" /> :
|
||||
<i className="icon-expand" />
|
||||
}
|
||||
</button>
|
||||
</aside>
|
||||
|
||||
<div
|
||||
className="question-text"
|
||||
dangerouslySetInnerHTML={{ __html: this.props.questionText }}
|
||||
/>
|
||||
</header>
|
||||
|
||||
<div>
|
||||
<section className="correct-answer-ratio-section">
|
||||
<CorrectAnswerDonut
|
||||
correctResponseRatio={this.state.correctResponseRatio}>
|
||||
<p><strong>{I18n.t('correct_answer', 'Correct answer')}</strong></p>
|
||||
<p>{correctResponseRatioLabel}</p>
|
||||
</CorrectAnswerDonut>
|
||||
</section>
|
||||
|
||||
<section className="answer-distribution-section">
|
||||
<AnswerBars answers={this.props.answers} />
|
||||
</section>
|
||||
<section className="discrimination-index-section">
|
||||
<DiscriminationIndex
|
||||
discriminationIndex={this.props.discriminationIndex}
|
||||
topStudentCount={this.props.topStudentCount}
|
||||
middleStudentCount={this.props.middleStudentCount}
|
||||
bottomStudentCount={this.props.bottomStudentCount}
|
||||
correctTopStudentCount={this.props.correctTopStudentCount}
|
||||
correctMiddleStudentCount={this.props.correctMiddleStudentCount}
|
||||
correctBottomStudentCount={this.props.correctBottomStudentCount}
|
||||
/>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
</Question>
|
||||
);
|
||||
},
|
||||
|
||||
toggleDetails: function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
this.setState({
|
||||
showingDetails: !this.state.showingDetails
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return MultipleChoice;
|
||||
});
|
|
@ -0,0 +1,98 @@
|
|||
/** @jsx React.DOM */
|
||||
define(function(require) {
|
||||
var React = require('react');
|
||||
var I18n = require('i18n!quiz_statistics.summary');
|
||||
var ScorePercentileChart = require('jsx!../components/score_percentile_chart');
|
||||
var secondsToTime = require('../util/seconds_to_time');
|
||||
var round = require('../util/round');
|
||||
var Report = require('jsx!./summary/report');
|
||||
|
||||
var Summary = React.createClass({
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
quizReports: [],
|
||||
pointsPossible: 0,
|
||||
scoreAverage: 0,
|
||||
scoreHigh: 0,
|
||||
scoreLow: 0,
|
||||
scoreStdev: 0,
|
||||
durationAverage: 0,
|
||||
scores: {}
|
||||
};
|
||||
},
|
||||
|
||||
ratioFor: function(score) {
|
||||
var quizPoints = parseFloat(this.props.pointsPossible);
|
||||
|
||||
if (quizPoints > 0) {
|
||||
return round(score / quizPoints * 100.0, 0);
|
||||
}
|
||||
else {
|
||||
return 0;
|
||||
}
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return(
|
||||
<div id="summary-statistics">
|
||||
<header className="padded">
|
||||
<h3 className="section-title inline">{I18n.t('quiz_summary', 'Quiz Summary')}</h3>
|
||||
|
||||
<aside className="pull-right">
|
||||
{this.props.quizReports.map(this.renderReport)}
|
||||
</aside>
|
||||
</header>
|
||||
|
||||
<table className="text-left">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<i className="icon-quiz-stats-avg"></i>{' '}
|
||||
{I18n.t('stats_mean', 'Avg Score')}
|
||||
</th>
|
||||
<th>
|
||||
<i className="icon-quiz-stats-high"></i>{' '}
|
||||
{I18n.t('stats_high', 'High Score')}
|
||||
</th>
|
||||
<th>
|
||||
<i className="icon-quiz-stats-low"></i>{' '}
|
||||
{I18n.t('stats_low', 'Low Score')}
|
||||
</th>
|
||||
<th>
|
||||
<i className="icon-quiz-stats-deviation"></i>{' '}
|
||||
{I18n.t('stats_stdev', 'Std. Deviation')}
|
||||
</th>
|
||||
<th>
|
||||
<i className="icon-quiz-stats-time"></i>{' '}
|
||||
{I18n.t('stats_avg_time', 'Avg Time')}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className="emphasized">
|
||||
{this.ratioFor(this.props.scoreAverage)}%
|
||||
</td>
|
||||
<td>{this.ratioFor(this.props.scoreHigh)}%</td>
|
||||
<td>{this.ratioFor(this.props.scoreLow)}%</td>
|
||||
<td>{round(this.props.scoreStdev)}</td>
|
||||
<td>{secondsToTime(this.props.durationAverage)}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<ScorePercentileChart
|
||||
scores={this.props.scores} />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
||||
renderReport: function(reportProps) {
|
||||
reportProps.key = 'report-' + reportProps.id;
|
||||
return Report(reportProps);
|
||||
},
|
||||
});
|
||||
|
||||
return Summary;
|
||||
});
|
|
@ -0,0 +1,79 @@
|
|||
/** @jsx React.DOM */
|
||||
define(function(require) {
|
||||
var React = require('../../ext/react');
|
||||
var $ = require('canvas_packages/jquery');
|
||||
var Tooltip = require('canvas_packages/tooltip');
|
||||
|
||||
var Report = React.createClass({
|
||||
mixins: [ React.addons.ActorMixin ],
|
||||
|
||||
propTypes: {
|
||||
generatable: React.PropTypes.bool
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
tooltipContent: null
|
||||
};
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
readableType: 'Analysis Report',
|
||||
generatable: false,
|
||||
downloadUrl: undefined
|
||||
};
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
$(this.getDOMNode()).tooltip({
|
||||
content: function() {
|
||||
return this.state.tooltipContent;
|
||||
}.bind(this)
|
||||
});
|
||||
},
|
||||
|
||||
componentDidUpdate: function(prevProps, prevState) {
|
||||
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
$(this.getDOMNode()).tooltip('destroy');
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return (
|
||||
<div className="report-generator inline">{
|
||||
this.props.generatable ?
|
||||
this.renderGenerator() :
|
||||
this.renderDownloader()
|
||||
}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
||||
renderGenerator: function() {
|
||||
return (
|
||||
<button title="adooken" onClick={this.onGenerate} className="btn btn-link generate-report">
|
||||
<i className="icon-analytics" /> {this.props.readableType}
|
||||
</button>
|
||||
);
|
||||
},
|
||||
|
||||
renderDownloader: function() {
|
||||
return(
|
||||
<a href={this.props.downloadUrl} className="btn btn-link">
|
||||
<i className="icon-analytics" /> {this.props.readableType}
|
||||
</a>
|
||||
);
|
||||
},
|
||||
|
||||
onGenerate: function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
this.sendAction('statistics:generateReport', this.props.reportType);
|
||||
}
|
||||
});
|
||||
|
||||
return Report;
|
||||
});
|
|
@ -0,0 +1,16 @@
|
|||
module.exports = {
|
||||
description: 'Build an optimized version of the JavaScript sources.',
|
||||
runner: [
|
||||
'clean:compiled_symlink',
|
||||
'clean:compiled_jsx',
|
||||
'copy:src',
|
||||
'copy:map',
|
||||
'convert_jsx_i18n',
|
||||
'react:build',
|
||||
'symlink:compiled',
|
||||
'shim_canvas_packages',
|
||||
'requirejs',
|
||||
'clean:compiled_symlink',
|
||||
'clean:compiled_jsx',
|
||||
]
|
||||
};
|
|
@ -0,0 +1,22 @@
|
|||
var glob = require('glob');
|
||||
var fs = require('fs');
|
||||
var convert = require('canvas_react_i18n');
|
||||
|
||||
module.exports = {
|
||||
description: 'Convert <Text /> blocks in JSX to Canvas-compatible I18n calls.',
|
||||
runner: function(grunt) {
|
||||
var path = 'tmp/js/canvas_quiz_statistics';
|
||||
|
||||
glob.sync('**/*.jsx', { cwd: path }).forEach(function(fileName) {
|
||||
var filePath = path + '/' + fileName;
|
||||
var contents = String(fs.readFileSync(filePath));
|
||||
var newContents = convert(contents);
|
||||
|
||||
if (newContents !== contents) {
|
||||
console.log('Found <Text /> in', filePath);
|
||||
|
||||
fs.writeFileSync(filePath, newContents);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
|
@ -0,0 +1,7 @@
|
|||
module.exports = {
|
||||
description: 'Build a production-ready version.',
|
||||
runner: [
|
||||
'compile_js',
|
||||
'compile_css'
|
||||
]
|
||||
};
|
|
@ -0,0 +1,6 @@
|
|||
module.exports = {
|
||||
description: 'Build an optimized version of the CSS sources.',
|
||||
runner: [
|
||||
'sass'
|
||||
]
|
||||
};
|
|
@ -0,0 +1,7 @@
|
|||
module.exports = {
|
||||
description: 'Use the development, non-optimized JS sources.',
|
||||
runner: function(grunt) {
|
||||
grunt.task.run('symlink:development');
|
||||
grunt.task.run('compile_css');
|
||||
}
|
||||
};
|
|
@ -0,0 +1,7 @@
|
|||
module.exports = {
|
||||
description: 'Generate API documentation.',
|
||||
runner: [
|
||||
'react:dev',
|
||||
'jsduck'
|
||||
]
|
||||
};
|
|
@ -0,0 +1,62 @@
|
|||
var rewriteRulesSnippet = require('grunt-connect-rewrite/lib/utils').rewriteRequest;
|
||||
var proxy = require('grunt-connect-proxy/lib/utils').proxyRequest;
|
||||
|
||||
module.exports = {
|
||||
www: {
|
||||
proxies: [{
|
||||
context: '/api/v1',
|
||||
host: 'localhost',
|
||||
port: 3000,
|
||||
https: false,
|
||||
changeOrigin: false,
|
||||
xforward: false
|
||||
}],
|
||||
|
||||
options: {
|
||||
keepalive: false,
|
||||
port: 9442,
|
||||
base: 'www',
|
||||
middleware: function (connect, options) {
|
||||
var middlewares = [];
|
||||
var directory;
|
||||
|
||||
// ReverseProxy support
|
||||
middlewares.push( proxy );
|
||||
|
||||
// RewriteRules support
|
||||
middlewares.push(rewriteRulesSnippet);
|
||||
|
||||
if (!Array.isArray(options.base)) {
|
||||
options.base = [options.base];
|
||||
}
|
||||
|
||||
// Serve static files.
|
||||
options.base.forEach(function (base) {
|
||||
middlewares.push(connect.static(base));
|
||||
});
|
||||
|
||||
// Make directory browse-able.
|
||||
directory = options.directory || options.base[options.base.length - 1];
|
||||
middlewares.push(connect.directory(directory));
|
||||
|
||||
return middlewares;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
tests: {
|
||||
options: {
|
||||
keepalive: false,
|
||||
port: 9443,
|
||||
hostname: '*'
|
||||
}
|
||||
},
|
||||
|
||||
docs: {
|
||||
options: {
|
||||
keepalive: true,
|
||||
port: 9444,
|
||||
base: "doc"
|
||||
}
|
||||
},
|
||||
};
|
|
@ -0,0 +1,35 @@
|
|||
module.exports = {
|
||||
unit: {
|
||||
options : {
|
||||
timeout: 10000,
|
||||
outfile: 'tests.html',
|
||||
|
||||
host: 'http://127.0.0.1:<%= grunt.config.get("connect.tests.options.port") %>/',
|
||||
|
||||
template: require('grunt-template-jasmine-requirejs'),
|
||||
templateOptions: {
|
||||
requireConfigFile: [
|
||||
'src/js/main.js',
|
||||
'test/config.js',
|
||||
],
|
||||
deferHelpers: true,
|
||||
defaultErrors: true
|
||||
},
|
||||
|
||||
keepRunner: true,
|
||||
|
||||
version: '2.0.0',
|
||||
|
||||
styles: [ "www/dist/<%= grunt.moduleId %>.css" ],
|
||||
|
||||
helpers: [
|
||||
'test/support/*.js',
|
||||
'test/helpers/*.js',
|
||||
],
|
||||
|
||||
specs: [
|
||||
'test/unit/**/*.js'
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
|
@ -0,0 +1,20 @@
|
|||
var grunt = require('grunt');
|
||||
|
||||
module.exports = {
|
||||
main: {
|
||||
src: [ 'src/js', 'src/css', 'tmp/compiled/**/*.js' ],
|
||||
dest: 'doc/api',
|
||||
options: {
|
||||
'title': "<%= grunt.appName %> Reference",
|
||||
'builtin-classes': false,
|
||||
'color': true,
|
||||
'no-source': true,
|
||||
'tests': false,
|
||||
'processes': 2,
|
||||
'warnings': [],
|
||||
'external': [
|
||||
'React',
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
|
@ -0,0 +1,11 @@
|
|||
module.exports = {
|
||||
src: [ 'src/js/**/*.js' ],
|
||||
tests: [ 'test/**/*.js' ],
|
||||
jsx: [ 'tmp/compiled/jsx/**/*.js' ],
|
||||
options: {
|
||||
force: true,
|
||||
jshintrc: 'test/.jshintrc',
|
||||
'-W098': true,
|
||||
reporter: require('jshint-stylish-ex')
|
||||
}
|
||||
};
|
|
@ -0,0 +1,21 @@
|
|||
var grunt = require('grunt');
|
||||
|
||||
module.exports = {
|
||||
js: {
|
||||
options: {
|
||||
message: "<%= grunt.appName %> JS has been compiled.",
|
||||
}
|
||||
},
|
||||
|
||||
css: {
|
||||
options: {
|
||||
message: "<%= grunt.appName %> CSS has been compiled."
|
||||
}
|
||||
},
|
||||
|
||||
docs: {
|
||||
options: {
|
||||
message: "<%= grunt.appName %> API docs have been generated."
|
||||
}
|
||||
},
|
||||
};
|
|
@ -0,0 +1,15 @@
|
|||
module.exports = {
|
||||
dist: {
|
||||
options: {
|
||||
style: 'expanded',
|
||||
includePaths: [
|
||||
'vendor/canvas/app/stylesheets',
|
||||
'vendor/canvas/app/stylesheets/variants/new_styles_normal_contrast'
|
||||
],
|
||||
outputStyle: 'nested'
|
||||
},
|
||||
files: {
|
||||
'dist/canvas_quiz_statistics.css': 'src/css/app.scss',
|
||||
}
|
||||
}
|
||||
};
|
|
@ -0,0 +1,33 @@
|
|||
module.exports = {
|
||||
options: {
|
||||
spawn: false,
|
||||
},
|
||||
|
||||
css: {
|
||||
files: '{src,vendor}/css/**/*.{scss,css}',
|
||||
tasks: [ 'compile_css' ],
|
||||
options: {
|
||||
spawn: true
|
||||
}
|
||||
},
|
||||
|
||||
compiled_css: {
|
||||
files: 'dist/*.css',
|
||||
tasks: [ 'noop', 'notify:css' ],
|
||||
options: {
|
||||
livereload: {
|
||||
port: 9224
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
jsx: {
|
||||
files: 'src/js/**/*.jsx',
|
||||
tasks: [ 'newer:react:dev', 'jshint:jsx' ]
|
||||
},
|
||||
|
||||
tests: {
|
||||
files: [ 'src/js/**/*.j{s,sx}', 'test/**/*', 'tasks/*.js', ],
|
||||
tasks: [ 'jasmine:unit' ],
|
||||
},
|
||||
};
|
|
@ -0,0 +1,38 @@
|
|||
var shell = require('shelljs');
|
||||
var glob = require('glob');
|
||||
var printHelp = function() {
|
||||
console.log('Usage: grunt report:TARGET');
|
||||
console.log('\nAvailable targets:');
|
||||
console.log(' - "lodash_methods": print all the used lodash methods');
|
||||
};
|
||||
|
||||
var printAvailablePackages = function() {
|
||||
var PKG_PATH = 'vendor/packages';
|
||||
var pkgNames = glob.sync('**/*.js', { cwd: PKG_PATH }).reduce(function(set, pkg) {
|
||||
var pkgName = pkg.replace(/\.js$/, '');
|
||||
return set.concat(pkgName);
|
||||
}, []);
|
||||
|
||||
console.log('There are', pkgNames.length, 'available packages:\n');
|
||||
|
||||
pkgNames.forEach(function(pkgName, index) {
|
||||
console.log(' ' + (index+1) + '.', pkgName);
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
description: 'Use the development, non-optimized JS sources.',
|
||||
runner: function(grunt, target) {
|
||||
switch(target) {
|
||||
case 'lodash_methods':
|
||||
shell.exec("echo 'Reporting used lodash methods:'");
|
||||
shell.exec("grep -rPoh '_\\.[^\\b|\\(|;]+' src/js/ | sort | uniq");
|
||||
break;
|
||||
case 'available_packages':
|
||||
printAvailablePackages();
|
||||
break;
|
||||
default:
|
||||
printHelp();
|
||||
}
|
||||
}
|
||||
};
|
|
@ -0,0 +1,11 @@
|
|||
module.exports = {
|
||||
description: 'Serve the application using a local Connect server.',
|
||||
runner: function(grunt, target) {
|
||||
grunt.task.run([
|
||||
'development',
|
||||
'configureRewriteRules',
|
||||
'configureProxies:www',
|
||||
'connect:www'
|
||||
]);
|
||||
}
|
||||
};
|
|
@ -0,0 +1,9 @@
|
|||
module.exports = {
|
||||
description: 'Run the Jasmine unit tests.',
|
||||
runner: function(grunt, target) {
|
||||
grunt.task.run('symlink:assets');
|
||||
grunt.task.run('connect:tests');
|
||||
grunt.task.run('jasmine:' + (target || ''));
|
||||
grunt.task.run('clean:assets');
|
||||
}
|
||||
};
|
|
@ -0,0 +1,3 @@
|
|||
module.exports = {
|
||||
runner: function() {}
|
||||
};
|
|
@ -0,0 +1,10 @@
|
|||
module.exports = {
|
||||
options: {
|
||||
force: false
|
||||
},
|
||||
|
||||
compiled_symlink: [ 'tmp/js/canvas_quiz_statistics/compiled' ],
|
||||
compiled_jsx: [ 'tmp/compiled' ],
|
||||
compiled_assets: [ 'tmp/assets' ],
|
||||
assets: [ './assets' ]
|
||||
};
|
|
@ -0,0 +1,18 @@
|
|||
module.exports = {
|
||||
src: {
|
||||
files: [
|
||||
{
|
||||
expand: true,
|
||||
cwd: 'src/js/',
|
||||
src: '**/*',
|
||||
dest: 'tmp/js/canvas_quiz_statistics'
|
||||
}
|
||||
]
|
||||
},
|
||||
map: {
|
||||
files: [{
|
||||
src: 'vendor/packages/map.json',
|
||||
dest: 'dist/canvas_quiz_statistics.map.json'
|
||||
}]
|
||||
}
|
||||
};
|
|
@ -0,0 +1,38 @@
|
|||
// Rename all files to end with .js; source can be .jsx, or .hm.jsx
|
||||
var rename = function (dest, src) {
|
||||
var folder = src.substring(0, src.lastIndexOf('/'));
|
||||
var filename = src.substring(src.lastIndexOf('/'), src.length);
|
||||
var extIndex = filename.lastIndexOf('.');
|
||||
var extension;
|
||||
|
||||
extension = filename.substring(extIndex+1, filename.length);
|
||||
filename = filename.substring(0, extIndex);
|
||||
|
||||
return dest +'/' + folder + filename + '.' + extension.replace(/jsx/, 'js');
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
dev: {
|
||||
files: [{
|
||||
expand: true,
|
||||
cwd: 'src/js',
|
||||
src: [ '**/*.jsx' ],
|
||||
dest: 'tmp/compiled/jsx',
|
||||
rename: rename
|
||||
}]
|
||||
},
|
||||
|
||||
build: {
|
||||
options: {
|
||||
// ignoreMTime: true
|
||||
},
|
||||
|
||||
files: [{
|
||||
expand: true,
|
||||
cwd: 'tmp/js/canvas_quiz_statistics',
|
||||
src: [ '**/*.jsx' ],
|
||||
dest: 'tmp/compiled/jsx',
|
||||
rename: rename
|
||||
}]
|
||||
}
|
||||
};
|
|
@ -0,0 +1,85 @@
|
|||
var grunt = require('grunt');
|
||||
var _ = require('lodash');
|
||||
var convert = require('rjs_converter');
|
||||
var merge = _.merge;
|
||||
|
||||
var baseOptions = {
|
||||
baseUrl: 'tmp/js',
|
||||
mainConfigFile: "tmp/js/<%= grunt.moduleId %>/main.js",
|
||||
optimize: 'none',
|
||||
|
||||
removeCombined: false,
|
||||
inlineText: true,
|
||||
preserveLicenseComments: false,
|
||||
|
||||
pragmas: {
|
||||
production: true
|
||||
},
|
||||
|
||||
jsx: {
|
||||
moduleId: grunt.moduleId
|
||||
},
|
||||
|
||||
paths: {
|
||||
'lodash': 'empty:',
|
||||
'react': 'empty:',
|
||||
'd3': 'empty:',
|
||||
},
|
||||
|
||||
wrap: {
|
||||
start: [
|
||||
// The following declaration must be set at the very first line of the
|
||||
// output for i18n extraction to allow multiple scopes within the same
|
||||
// file.
|
||||
"/* canvas_precompiled_asset: amd */",
|
||||
|
||||
// App name and version, for cools.
|
||||
"/* <%= grunt.moduleId %> <%= grunt.config.get('pkg.version') %> */",
|
||||
|
||||
""
|
||||
].join("\n")
|
||||
},
|
||||
|
||||
rawText: {
|
||||
},
|
||||
|
||||
name: "<%= grunt.moduleId %>",
|
||||
include: [ "<%= grunt.moduleId %>/boot" ],
|
||||
exclude: [ 'text', 'jsx', 'i18n' ],
|
||||
|
||||
onBuildWrite: function(moduleName, modulePath, contents) {
|
||||
return convert(contents
|
||||
// Text and JSX modules get inlined by the post-processor and become
|
||||
// regular modules so get rid of the plugin prefix in module ids:
|
||||
.replace(/(text!|jsx!)/g, '')
|
||||
|
||||
// Rewrite all modules that start with "canvas_packages/" to be without
|
||||
// that prefix since when they're embedded in Canvas, the module IDs will
|
||||
// just match:
|
||||
.replace(/(['"])canvas_packages\//g, "$1")
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// Alias "boot" to the module id:
|
||||
//
|
||||
// This allows to do the following for an app named "canvas_quizzes":
|
||||
//
|
||||
// require([ 'canvas_quizzes' ], function(app) {
|
||||
// app.mount(document.body);
|
||||
// });
|
||||
//
|
||||
// Instead of:
|
||||
//
|
||||
// require([ 'canvas_quizzes/boot' ]);
|
||||
baseOptions.rawText[grunt.moduleId] =
|
||||
"define(['<%= grunt.moduleId %>/boot'], function(arg) { return arg; });";
|
||||
|
||||
module.exports = {
|
||||
debug: {
|
||||
options: merge({}, baseOptions, {
|
||||
optimize: 'none',
|
||||
out: "dist/<%= grunt.moduleId %>.js"
|
||||
})
|
||||
},
|
||||
};
|
|
@ -0,0 +1,38 @@
|
|||
module.exports = {
|
||||
options: {
|
||||
overwrite: true
|
||||
},
|
||||
|
||||
compiled: {
|
||||
files: [{
|
||||
src: 'tmp/compiled',
|
||||
dest: 'tmp/js/canvas_quiz_statistics/compiled'
|
||||
}],
|
||||
},
|
||||
|
||||
development: {
|
||||
options: {
|
||||
overwrite: false
|
||||
},
|
||||
|
||||
files: [{
|
||||
src: '../../',
|
||||
dest: 'vendor/canvas'
|
||||
}, {
|
||||
expand: true,
|
||||
src: '{src,dist,vendor}',
|
||||
dest: 'www/',
|
||||
}, {
|
||||
expand: true,
|
||||
cwd: '../../public',
|
||||
src: '{font,images}',
|
||||
dest: 'www/'
|
||||
}, {
|
||||
src: 'test/fixtures',
|
||||
dest: 'www/fixtures'
|
||||
}]
|
||||
},
|
||||
|
||||
assets: {
|
||||
}
|
||||
};
|
|
@ -0,0 +1,40 @@
|
|||
/* jshint node:true */
|
||||
|
||||
var glob = require('glob');
|
||||
var PKG_PATH = 'vendor/packages';
|
||||
var PKG_PATH_RJS = '../../vendor/packages';
|
||||
var _ = require('lodash');
|
||||
var merge = _.merge;
|
||||
var keys = _.keys;
|
||||
|
||||
module.exports = {
|
||||
description: 'Exclude Canvas packages from the distributable version of the app.',
|
||||
runner: function(grunt) {
|
||||
// Look for all the packages in vendor/packages/**/*.js and create an "empty:"
|
||||
// map containing all the package paths.
|
||||
//
|
||||
// For example, the following files will produce the map below:
|
||||
// /vendor/packages/jquery.js
|
||||
// /vendor/packages/jqueryui/dialog.js
|
||||
//
|
||||
// {
|
||||
// "../../vendor/packages/jquery": "empty:",
|
||||
// "../../vendor/packages/jqueryui/dialog": "empty:",
|
||||
// }
|
||||
//
|
||||
var pkgMap = glob.sync('**/*.js', { cwd: PKG_PATH }).reduce(function(set, pkg) {
|
||||
var pkgName = pkg.replace(/\.js$/, '');
|
||||
set[PKG_PATH_RJS + '/' + pkgName] = 'empty:';
|
||||
return set;
|
||||
}, {});
|
||||
|
||||
// Go through each requirejs build target and munge the "paths" map with
|
||||
// our shimmed packages one:
|
||||
keys(grunt.config.get('requirejs')).forEach(function(target) {
|
||||
var configKey = [ 'requirejs', target, 'options' ].join('.');
|
||||
var targetOptions = grunt.config.get(configKey);
|
||||
|
||||
grunt.config.set(configKey, merge({}, targetOptions, { paths: pkgMap }));
|
||||
});
|
||||
}
|
||||
};
|
|
@ -0,0 +1,97 @@
|
|||
{
|
||||
"maxerr" : 50,
|
||||
|
||||
"bitwise" : true,
|
||||
"camelcase" : false,
|
||||
"curly" : true,
|
||||
"eqeqeq" : false,
|
||||
"forin" : true,
|
||||
"immed" : false,
|
||||
"indent" : false,
|
||||
"latedef" : true,
|
||||
"newcap" : false,
|
||||
"noarg" : true,
|
||||
"noempty" : true,
|
||||
"nonew" : false,
|
||||
"plusplus" : false,
|
||||
"quotmark" : false,
|
||||
|
||||
"undef" : true,
|
||||
"unused" : true,
|
||||
"strict" : false,
|
||||
"trailing" : false,
|
||||
"maxparams" : false,
|
||||
"maxdepth" : false,
|
||||
"maxstatements" : false,
|
||||
"maxcomplexity" : false,
|
||||
"maxlen" : false,
|
||||
|
||||
"asi" : false,
|
||||
"boss" : false,
|
||||
"debug" : false,
|
||||
"eqnull" : false,
|
||||
"es5" : false,
|
||||
"esnext" : false,
|
||||
"moz" : false,
|
||||
|
||||
"evil" : false,
|
||||
"expr" : false,
|
||||
"funcscope" : false,
|
||||
"globalstrict" : false,
|
||||
"iterator" : false,
|
||||
"lastsemic" : false,
|
||||
"laxbreak" : false,
|
||||
"laxcomma" : false,
|
||||
"loopfunc" : false,
|
||||
"multistr" : false,
|
||||
"proto" : false,
|
||||
"scripturl" : false,
|
||||
"smarttabs" : false,
|
||||
"shadow" : false,
|
||||
"sub" : false,
|
||||
"supernew" : false,
|
||||
"validthis" : false,
|
||||
|
||||
"browser" : true,
|
||||
"couch" : false,
|
||||
"devel" : true,
|
||||
"dojo" : false,
|
||||
"jquery" : false,
|
||||
"mootools" : false,
|
||||
"node" : false,
|
||||
"nonstandard" : false,
|
||||
"prototypejs" : false,
|
||||
"rhino" : false,
|
||||
"worker" : false,
|
||||
"wsh" : false,
|
||||
"yui" : false,
|
||||
|
||||
"nomen" : false,
|
||||
"onevar" : false,
|
||||
"passfail" : false,
|
||||
"white" : false,
|
||||
|
||||
"globals" : {
|
||||
"define": false,
|
||||
"require": false,
|
||||
"waitsFor": false,
|
||||
"runs": false,
|
||||
"it" : false,
|
||||
"xit" : false,
|
||||
"describe" : false,
|
||||
"xdescribe" : false,
|
||||
"beforeEach" : false,
|
||||
"afterEach" : false,
|
||||
"expect" : false,
|
||||
"spyOn" : false,
|
||||
"Fixtures": false,
|
||||
"ContextError": false,
|
||||
"jasmine": false,
|
||||
"subject": false,
|
||||
"find": false,
|
||||
"click": false,
|
||||
"fillIn": false,
|
||||
"setProps": false,
|
||||
"findAll": false
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue