modernize canvas_quizzes
fixes FOO-1409 flag = none no more client_apps, canvas_quizzes now lives as part of canvas-lms proper inside app/jsx/, which makes the build leaner and leaves us with one less thing to reason about logical changes: - converted from AMD to ES modules - upgraded to recent react + react-router - dropped RSVP in favor of native Promises - used CanvasModal instead of home-grown Dialog - removed dead code; notifications in particular were fishy as there had no dependents at all and did not even show up in the graph - ported tests to Jest, added more unit ones and two integration ones - removed "config.onError" and now throws errors where appropriate - disabled console statements in non-dev :: test plan :: - create a (old-school) quiz containing all types of questions - as 3 distinct students, take the quiz and try to randomize your answers at this point it's helpful to have a reference to compare the screens; I replicated the quiz on my production sandbox for this - go to /courses/:id/quizzes/:id/submissions/:id/log - verify it looks OK - click on a specific question in the stream and verify the question inspector widget works OK - go back to stream and push "View table" - verify the table and its controls are OK - go to /courses/:id/quizzes/:id/statistics - verify it looks OK - click on ? in the discrimination index chart and verify it displays a dialog with help content - click on "X respondents" in one of the charts and verify it displays a dialog with the respondent names - verify the interactive charts do interact as expected (no logic changed here so just a quick glance) - link to "View in SpeedGrader" for essay-like questions works Change-Id: I79af5ff4f1479503b5e2528b613255dde5bc45d3 Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/256118 Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com> Reviewed-by: Simon Williams <simon@instructure.com> QA-Review: Simon Williams <simon@instructure.com> Product-Review: Simon Williams <simon@instructure.com>
This commit is contained in:
parent
a4e1da3aea
commit
46f8efd61f
|
@ -16,9 +16,6 @@ docker-compose/
|
|||
docker-compose.local.*.yml
|
||||
docker-compose.override.yml
|
||||
|
||||
client_apps/canvas_quizzes/dist/
|
||||
client_apps/canvas_quizzes/node_modules/
|
||||
client_apps/canvas_quizzes/tmp/
|
||||
coverage/
|
||||
coverage-js/
|
||||
gems/*/node_modules/
|
||||
|
@ -32,7 +29,6 @@ packages/*/node_modules/
|
|||
packages/canvas-planner/lib
|
||||
public/dist/
|
||||
public/doc/
|
||||
public/javascripts/client_apps/
|
||||
public/javascripts/translations/
|
||||
!public/javascripts/translations/_core_en.js
|
||||
tmp/*
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
/app/jsx/canvas_quizzes/**/*.test.js
|
||||
/gems/canvas_i18nliner/*
|
||||
/doc/*
|
||||
/public/javascripts/translations/*
|
||||
|
@ -5,7 +6,6 @@
|
|||
/public/javascripts/bower/*
|
||||
public/javascripts/react-dnd-test-backend.js
|
||||
public/javascripts/mathquill.js
|
||||
public/javascripts/symlink_to_node_modules/*
|
||||
|
||||
spec/selenium/helpers/jquery.simulate.js
|
||||
spec/javascripts/support/jquery.mockjax.js
|
||||
|
|
60
.eslintrc.js
60
.eslintrc.js
|
@ -97,6 +97,55 @@ module.exports = {
|
|||
'react/no-render-return-value': 'warn', // In future versions of react this will fail
|
||||
'react/state-in-constructor': 'off',
|
||||
'react/static-property-placement': 'off',
|
||||
|
||||
// don't restrict Math.pow for ** operator
|
||||
// ref: https://github.com/airbnb/javascript/blob/1f786e154f6c32385607e1688370d7f2d053f88f/packages/eslint-config-airbnb-base/rules/best-practices.js#L225
|
||||
'no-restricted-properties': ['error',
|
||||
{
|
||||
object: 'arguments',
|
||||
property: 'callee',
|
||||
message: 'arguments.callee is deprecated',
|
||||
},
|
||||
{
|
||||
object: 'global',
|
||||
property: 'isFinite',
|
||||
message: 'Please use Number.isFinite instead',
|
||||
},
|
||||
{
|
||||
object: 'self',
|
||||
property: 'isFinite',
|
||||
message: 'Please use Number.isFinite instead',
|
||||
},
|
||||
{
|
||||
object: 'window',
|
||||
property: 'isFinite',
|
||||
message: 'Please use Number.isFinite instead',
|
||||
},
|
||||
{
|
||||
object: 'global',
|
||||
property: 'isNaN',
|
||||
message: 'Please use Number.isNaN instead',
|
||||
},
|
||||
{
|
||||
object: 'self',
|
||||
property: 'isNaN',
|
||||
message: 'Please use Number.isNaN instead',
|
||||
},
|
||||
{
|
||||
object: 'window',
|
||||
property: 'isNaN',
|
||||
message: 'Please use Number.isNaN instead',
|
||||
},
|
||||
{
|
||||
property: '__defineGetter__',
|
||||
message: 'Please use Object.defineProperty instead.',
|
||||
},
|
||||
{
|
||||
property: '__defineSetter__',
|
||||
message: 'Please use Object.defineProperty instead.',
|
||||
},
|
||||
],
|
||||
|
||||
'no-restricted-syntax': [
|
||||
// This is here because we are turning off 2 items from what AirBnB cares about.
|
||||
'error',
|
||||
|
@ -183,6 +232,15 @@ module.exports = {
|
|||
'import/no-unresolved': 'off',
|
||||
'import/no-webpack-loader-syntax': 'off'
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
files: ['app/jsx/canvas_quizzes/**/*'],
|
||||
rules: {
|
||||
'react/prop-types': 'off',
|
||||
'prefer-const': 'warn',
|
||||
'prettier/prettier': 'off',
|
||||
'react/no-string-refs': 'warn',
|
||||
}
|
||||
},
|
||||
]
|
||||
}
|
||||
|
|
|
@ -52,9 +52,6 @@ yarn-error.log
|
|||
/spec/coffeescripts/plugins/
|
||||
/spec/plugins/
|
||||
|
||||
# generated client app stuff
|
||||
/public/javascripts/client_apps/
|
||||
|
||||
# canvas-gems
|
||||
/gems/*/coverage
|
||||
/gems/*/tmp
|
||||
|
|
1
.i18nrc
1
.i18nrc
|
@ -16,7 +16,6 @@
|
|||
|
||||
"include": [
|
||||
"app/coffeescripts/.i18nrc",
|
||||
"client_apps/canvas_quizzes/.i18nrc",
|
||||
"gems/plugins/.i18nrc",
|
||||
"public/javascripts/.i18nrc"
|
||||
]
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
{
|
||||
"ignoreFiles": [
|
||||
"app/stylesheets/vendor/**",
|
||||
"client_apps/canvas_quizzes/vendor/**",
|
||||
"public/**",
|
||||
],
|
||||
"rules": {
|
||||
|
|
|
@ -66,9 +66,6 @@ RUN set -eux; \
|
|||
.yardoc \
|
||||
app/stylesheets/brandable_css_brands \
|
||||
app/views/info \
|
||||
client_apps/canvas_quizzes/dist \
|
||||
client_apps/canvas_quizzes/node_modules \
|
||||
client_apps/canvas_quizzes/tmp \
|
||||
config/locales/generated \
|
||||
gems/canvas_i18nliner/node_modules \
|
||||
log \
|
||||
|
@ -92,7 +89,6 @@ RUN set -eux; \
|
|||
pacts \
|
||||
public/dist \
|
||||
public/doc/api \
|
||||
public/javascripts/client_apps \
|
||||
public/javascripts/compiled \
|
||||
public/javascripts/translations \
|
||||
reports \
|
||||
|
|
|
@ -30,7 +30,6 @@ RUN --mount=target=/tmp/src \
|
|||
\
|
||||
/tmp/dst && \
|
||||
find \
|
||||
client_apps/* \
|
||||
gems/canvas_i18nliner \
|
||||
gems/plugins/* \
|
||||
packages/* \
|
||||
|
@ -76,7 +75,6 @@ RUN --mount=target=/tmp/src \
|
|||
app/stylesheets \
|
||||
app/views/jst \
|
||||
bin \
|
||||
client_apps \
|
||||
config/environments \
|
||||
config/locales \
|
||||
frontend_build \
|
||||
|
@ -92,7 +90,6 @@ RUN --mount=target=/tmp/src \
|
|||
config/canvas_rails_switcher.rb \
|
||||
config/environment.rb \
|
||||
config/initializers/plugin_symlinks.rb \
|
||||
config/initializers/client_app_symlinks.rb \
|
||||
config/initializers/json.rb \
|
||||
config/initializers/revved_asset_urls.rb \
|
||||
db/migrate/*_regenerate_brand_files_based_on_new_defaults_*.rb \
|
||||
|
|
|
@ -137,10 +137,6 @@ pipeline {
|
|||
}
|
||||
}
|
||||
|
||||
tests['canvas_quizzes'] = {
|
||||
sh 'build/new-jenkins/js/tests-quizzes.sh'
|
||||
}
|
||||
|
||||
for(int i = 0; i < JSG_NODE_COUNT; i++) {
|
||||
tests["Karma - Spec Group - jsg${i}"] = makeKarmaStage('jsg', i, JSG_NODE_COUNT)
|
||||
}
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
/canvas_quizzes/test
|
|
@ -16,4 +16,17 @@
|
|||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import 'quiz_statistics_cqs'
|
||||
import $ from 'jquery'
|
||||
import { configure, mount } from '../canvas_quizzes/statistics/main.js'
|
||||
|
||||
configure({
|
||||
ajax: $.ajax,
|
||||
loadOnStartup: true,
|
||||
quizStatisticsUrl: ENV.quiz_statistics_url,
|
||||
quizReportsUrl: ENV.quiz_reports_url,
|
||||
courseSectionsUrl: ENV.course_sections_url
|
||||
})
|
||||
|
||||
mount(document.body.querySelector('#content')).then(() => {
|
||||
console.log('Yeah!!!')
|
||||
})
|
||||
|
|
|
@ -17,11 +17,11 @@
|
|||
*/
|
||||
|
||||
import $ from 'jquery'
|
||||
import app from 'canvas_quizzes/apps/events'
|
||||
import { configure, mount } from '../canvas_quizzes/events/main.js'
|
||||
import ready from '@instructure/ready'
|
||||
|
||||
ready(() => {
|
||||
app.configure({
|
||||
configure({
|
||||
ajax: $.ajax,
|
||||
loadOnStartup: true,
|
||||
quizUrl: ENV.quiz_url,
|
||||
|
@ -31,7 +31,7 @@ ready(() => {
|
|||
allowMatrixView: ENV.can_view_answer_audits
|
||||
})
|
||||
|
||||
app
|
||||
.mount(document.body.querySelector('#content'))
|
||||
.then(() => console.log('Yeah, a canvas quiz app has been loaded!!!'))
|
||||
mount(document.body.querySelector('#content')).then(() =>
|
||||
console.log('Yeah, a canvas quiz app has been loaded!!!')
|
||||
)
|
||||
})
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (C) 2014 - present Instructure, Inc.
|
||||
* Copyright (C) 2021 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
|
@ -16,23 +16,21 @@
|
|||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
define(function(require) {
|
||||
var $ = require('jquery');
|
||||
import Dispatcher from './core/dispatcher'
|
||||
import EventStore from './stores/events'
|
||||
|
||||
// We're already logging errors in config/initializers/rsvp.js
|
||||
if(typeof(jasmine) !== "undefined" && typeof(jasmine !== undefined)){
|
||||
jasmine.RSVP.logRSVPErrors = false;
|
||||
}
|
||||
const Actions = {}
|
||||
|
||||
return {
|
||||
ajax: $.ajax,
|
||||
Actions.dismissNotification = function(key) {
|
||||
return Dispatcher.dispatch('notifications:dismiss', key)
|
||||
}
|
||||
|
||||
xhr: {
|
||||
timeout: 25
|
||||
},
|
||||
Actions.reloadEvents = function() {
|
||||
EventStore.load()
|
||||
}
|
||||
|
||||
onError: function(message) {
|
||||
throw new Error(message);
|
||||
}
|
||||
};
|
||||
});
|
||||
Actions.setActiveAttempt = function(attempt) {
|
||||
EventStore.setActiveAttempt(attempt)
|
||||
}
|
||||
|
||||
export default Actions
|
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* Copyright (C) 2021 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas 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, version 3 of the License.
|
||||
*
|
||||
* Canvas 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/>.
|
||||
*/
|
||||
|
||||
import React from 'react'
|
||||
import {
|
||||
BrowserRouter,
|
||||
HashRouter,
|
||||
Switch,
|
||||
Route,
|
||||
} from 'react-router-dom'
|
||||
|
||||
import AnswerMatrixRoute from '../routes/answer_matrix'
|
||||
import AppRoute from '../routes/app'
|
||||
import EventStreamRoute from '../routes/event_stream'
|
||||
import QuestionRoute from '../routes/question'
|
||||
|
||||
export default function App(props) {
|
||||
const matches = window.location.pathname.match(/(.*\/log)/)
|
||||
const baseUrl = (matches && matches[0]) || ''
|
||||
const Router = props.useHashRouter ? HashRouter : BrowserRouter
|
||||
|
||||
return (
|
||||
<Router basename={baseUrl}>
|
||||
<Switch>
|
||||
<Route path="/questions/:id">
|
||||
<AppRoute>
|
||||
<QuestionRoute {...props} />
|
||||
</AppRoute>
|
||||
</Route>
|
||||
|
||||
<Route path="/answer_matrix">
|
||||
<AppRoute>
|
||||
<AnswerMatrixRoute {...props} />
|
||||
</AppRoute>
|
||||
</Route>
|
||||
|
||||
<Route path="/">
|
||||
<AppRoute>
|
||||
<EventStreamRoute {...props} />
|
||||
</AppRoute>
|
||||
</Route>
|
||||
|
||||
<Route path="*">
|
||||
<AppRoute />
|
||||
</Route>
|
||||
</Switch>
|
||||
</Router>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* Copyright (C) 2021 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas 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, version 3 of the License.
|
||||
*
|
||||
* Canvas 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/>.
|
||||
*/
|
||||
|
||||
import Backbone from 'backbone'
|
||||
import Event from '../models/event'
|
||||
import fromJSONAPI from '../../shared/models/common/from_jsonapi'
|
||||
import config from '../config'
|
||||
import PaginatedCollection from '../mixins/paginated_collection'
|
||||
|
||||
export default Backbone.Collection.extend({
|
||||
model: Event,
|
||||
// eslint-disable-next-line object-shorthand
|
||||
constructor: function() {
|
||||
PaginatedCollection(this)
|
||||
return Backbone.Collection.apply(this, arguments)
|
||||
},
|
||||
|
||||
url() {
|
||||
return config.eventsUrl
|
||||
},
|
||||
|
||||
parse(payload) {
|
||||
return fromJSONAPI(payload, 'quiz_submission_events')
|
||||
}
|
||||
})
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* Copyright (C) 2021 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas 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, version 3 of the License.
|
||||
*
|
||||
* Canvas 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/>.
|
||||
*/
|
||||
|
||||
import Backbone from 'backbone'
|
||||
import Question from '../models/question'
|
||||
import fromJSONAPI from '../../shared/models/common/from_jsonapi'
|
||||
import config from '../config'
|
||||
import PaginatedCollection from '../mixins/paginated_collection'
|
||||
|
||||
export default Backbone.Collection.extend({
|
||||
model: Question,
|
||||
// eslint-disable-next-line object-shorthand
|
||||
constructor: function() {
|
||||
PaginatedCollection(this)
|
||||
return Backbone.Collection.apply(this, arguments)
|
||||
},
|
||||
|
||||
url() {
|
||||
return config.questionsUrl
|
||||
},
|
||||
|
||||
parse(payload) {
|
||||
return fromJSONAPI(payload, 'quiz_questions')
|
||||
}
|
||||
})
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* Copyright (C) 2021 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas 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, version 3 of the License.
|
||||
*
|
||||
* Canvas 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/>.
|
||||
*/
|
||||
|
||||
import {render, fireEvent} from '@testing-library/react'
|
||||
import React from 'react'
|
||||
import Button from '../button'
|
||||
|
||||
const setup = props => {
|
||||
return render(
|
||||
<Button onClick={Function.prototype} {...props} />
|
||||
)
|
||||
}
|
||||
|
||||
describe('canvas_quizzes/components/button', () => {
|
||||
it('calls onClick on click', () => {
|
||||
const onClick = jest.fn()
|
||||
const {getByTestId} = setup({ onClick })
|
||||
expect(onClick.mock.calls.length).toBe(0)
|
||||
fireEvent.click(getByTestId('button'))
|
||||
expect(onClick.mock.calls.length).toBe(1)
|
||||
})
|
||||
})
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* Copyright (C) 2021 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas 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, version 3 of the License.
|
||||
*
|
||||
* Canvas 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/>.
|
||||
*/
|
||||
|
||||
import React from 'react'
|
||||
import classSet from '../../shared/util/class_set'
|
||||
|
||||
/**
|
||||
* @class Events.Components.Button
|
||||
*
|
||||
* A wrapper for `<button type="button" />` that abstracts the bootstrap CSS
|
||||
* classes we need to specify for buttons.
|
||||
*/
|
||||
const Button = ({children, onClick, type = 'default'}) => {
|
||||
const className = {}
|
||||
|
||||
className.btn = true
|
||||
className['btn-default'] = type === 'default'
|
||||
className['btn-danger'] = type === 'danger'
|
||||
className['btn-success'] = type === 'success'
|
||||
|
||||
return (
|
||||
<button
|
||||
data-testid="button"
|
||||
onClick={onClick}
|
||||
type="button"
|
||||
className={classSet(className)}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
export default Button
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (C) 2014 - present Instructure, Inc.
|
||||
* Copyright (C) 2021 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
|
@ -16,12 +16,11 @@
|
|||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
// define(function(require) {
|
||||
// var App = require('core/delegate');
|
||||
import config from './config/environments/production'
|
||||
import devConfig from './config/environments/development'
|
||||
|
||||
// describe('app', function() {
|
||||
// it('write me', function() {
|
||||
// expect(App).toBeTruthy();
|
||||
// });
|
||||
// });
|
||||
// });
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
Object.assign(config, devConfig)
|
||||
}
|
||||
|
||||
export default config
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* Copyright (C) 2021 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas 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, version 3 of the License.
|
||||
*
|
||||
* Canvas 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/>.
|
||||
*/
|
||||
|
||||
import $ from 'jquery'
|
||||
|
||||
export default {
|
||||
xhr: {
|
||||
timeout: 5000
|
||||
},
|
||||
|
||||
pollingFrequency: 500,
|
||||
|
||||
ajax: $.ajax,
|
||||
useHashRouter: false,
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
/*
|
||||
* Copyright (C) 2021 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas 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, version 3 of the License.
|
||||
*
|
||||
* Canvas 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/>.
|
||||
*/
|
||||
|
||||
export default {
|
||||
/**
|
||||
* @cfg {Function} ajax
|
||||
* An XHR request processor that has an API compatible with jQuery.ajax.
|
||||
*/
|
||||
ajax: undefined,
|
||||
|
||||
/**
|
||||
* @cfg {String} quizUrl
|
||||
* Canvas API endpoint for querying the current quiz.
|
||||
*/
|
||||
quizUrl: undefined,
|
||||
|
||||
/**
|
||||
* @cfg {String} submissionUrl
|
||||
* Canvas API endpoint for querying the current quiz submission.
|
||||
*/
|
||||
submissionUrl: undefined,
|
||||
|
||||
/**
|
||||
* @cfg {String} eventsUrl
|
||||
* Canvas API endpoint for querying the current quiz submission's events.
|
||||
*/
|
||||
eventsUrl: undefined,
|
||||
|
||||
/**
|
||||
* @cfg {String} questionsUrl
|
||||
* Canvas API endpoint for querying questions in the current quiz.
|
||||
*/
|
||||
questionsUrl: undefined,
|
||||
|
||||
attempt: 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,
|
||||
|
||||
/**
|
||||
* @cfg {Boolean} [allowMatrixView=true]
|
||||
*
|
||||
* Turn this off if you don't want the user to be able to view the answer
|
||||
* matrix.
|
||||
*/
|
||||
allowMatrixView: true,
|
||||
|
||||
useHashRouter: false,
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (C) 2014 - present Instructure, Inc.
|
||||
* Copyright (C) 2021 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
|
@ -16,11 +16,8 @@
|
|||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#HTMLReporter {
|
||||
position: relative;
|
||||
z-index: 0;
|
||||
}
|
||||
import './initializers/backbone'
|
||||
|
||||
.fixture {
|
||||
position: relative;
|
||||
export default function initializeApp() {
|
||||
return Promise.resolve()
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (C) 2014 - present Instructure, Inc.
|
||||
* Copyright (C) 2021 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
|
@ -16,20 +16,12 @@
|
|||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
define(function(require) {
|
||||
var $ = require('jquery');
|
||||
import Backbone from 'backbone'
|
||||
import config from '../../config'
|
||||
import CoreAdapter from '../../../shared/core/adapter'
|
||||
|
||||
return {
|
||||
xhr: {
|
||||
timeout: 5000
|
||||
},
|
||||
const Adapter = new CoreAdapter(config)
|
||||
|
||||
pollingFrequency: 500,
|
||||
|
||||
ajax: $.ajax,
|
||||
|
||||
onError: function(message) {
|
||||
throw new Error(message);
|
||||
}
|
||||
};
|
||||
});
|
||||
Backbone.ajax = function(options) {
|
||||
return Adapter.request(options)
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (C) 2014 - present Instructure, Inc.
|
||||
* Copyright (C) 2021 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
|
@ -16,7 +16,7 @@
|
|||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
define({
|
||||
export default {
|
||||
EVT_SESSION_STARTED: 'session_started',
|
||||
EVT_PAGE_FOCUSED: 'page_focused',
|
||||
EVT_PAGE_BLURRED: 'page_blurred',
|
||||
|
@ -27,32 +27,13 @@ define({
|
|||
EVT_FLAG_WARNING: 'warning',
|
||||
EVT_FLAG_OK: 'ok',
|
||||
|
||||
EVENT_ATTRS: [
|
||||
'id',
|
||||
'event_type',
|
||||
'event_data',
|
||||
'created_at',
|
||||
],
|
||||
EVENT_ATTRS: ['id', 'event_type', 'event_data', 'created_at'],
|
||||
|
||||
EVENT_DATA_ATTRS: [
|
||||
'quiz_question_id',
|
||||
'answer'
|
||||
],
|
||||
EVENT_DATA_ATTRS: ['quiz_question_id', 'answer'],
|
||||
|
||||
SUBMISSION_ATTRS: [
|
||||
'id',
|
||||
'started_at',
|
||||
'attempt'
|
||||
],
|
||||
SUBMISSION_ATTRS: ['id', 'started_at', 'attempt'],
|
||||
|
||||
QUESTION_ATTRS: [
|
||||
'id',
|
||||
'question_type',
|
||||
'question_text',
|
||||
'position',
|
||||
'answers',
|
||||
'matches'
|
||||
],
|
||||
QUESTION_ATTRS: ['id', 'question_type', 'question_text', 'position', 'answers', 'matches'],
|
||||
|
||||
Q_CALCULATED: 'calculated_question',
|
||||
Q_ESSAY: 'essay_question',
|
||||
|
@ -69,4 +50,4 @@ define({
|
|||
// Answer text longer than this will be truncated for questions of types
|
||||
// "essay" and other free-form input ones. This applies to the table view.
|
||||
MAX_VISIBLE_CHARS: 50
|
||||
});
|
||||
}
|
|
@ -0,0 +1,91 @@
|
|||
/*
|
||||
* Copyright (C) 2021 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas 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, version 3 of the License.
|
||||
*
|
||||
* Canvas 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/>.
|
||||
*/
|
||||
|
||||
import EventStore from '../stores/events'
|
||||
import config from '../config'
|
||||
|
||||
let update
|
||||
|
||||
/**
|
||||
* @class Events.Core.Controller
|
||||
* @private
|
||||
*
|
||||
* The controller is responsible for keeping the UI up-to-date with the
|
||||
* data layer.
|
||||
*/
|
||||
const Controller = {
|
||||
getState: () => ({
|
||||
useHashRouter: config.useHashRouter,
|
||||
submission: EventStore.getSubmission(),
|
||||
questions: EventStore.getQuestions(),
|
||||
events: EventStore.getAll(),
|
||||
isLoading: EventStore.isLoading(),
|
||||
attempt: EventStore.getAttempt(),
|
||||
availableAttempts: EventStore.getAvailableAttempts()
|
||||
}),
|
||||
|
||||
/**
|
||||
* 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(onUpdate) {
|
||||
update = () => {
|
||||
onUpdate(Controller.getState())
|
||||
}
|
||||
|
||||
EventStore.addChangeListener(update)
|
||||
|
||||
if (config.loadOnStartup) {
|
||||
return Controller.load()
|
||||
} else {
|
||||
return Promise.resolve()
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Load initial application data; quiz statistics and reports.
|
||||
*/
|
||||
load() {
|
||||
return EventStore.loadInitialData().then(EventStore.load.bind(EventStore))
|
||||
},
|
||||
|
||||
/**
|
||||
* Stop listening to data changes.
|
||||
*/
|
||||
stop() {
|
||||
if (update) {
|
||||
EventStore.removeChangeListener(update)
|
||||
update = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default Controller
|
|
@ -0,0 +1,97 @@
|
|||
/*
|
||||
* Copyright (C) 2021 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas 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, version 3 of the License.
|
||||
*
|
||||
* Canvas 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/>.
|
||||
*/
|
||||
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import _ from 'lodash.underscore'
|
||||
import config from '../config'
|
||||
import initialize from '../config/initializer'
|
||||
import Layout from '../bundles/routes'
|
||||
import controller from './controller'
|
||||
|
||||
const extend = _.extend
|
||||
let container
|
||||
|
||||
/**
|
||||
* @class Events.Core.Delegate
|
||||
*
|
||||
* The client app delegate. This is the main interface that embedding
|
||||
* applications use to interact with the client app.
|
||||
*/
|
||||
const exports = {}
|
||||
|
||||
/**
|
||||
* Configure the application. See Config for the supported options.
|
||||
*
|
||||
* @param {Object} options
|
||||
* A set of options to override.
|
||||
*/
|
||||
const 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 {Promise}
|
||||
* Fulfilled when the app has been started and rendered.
|
||||
*/
|
||||
const mount = function(node, options) {
|
||||
configure(options)
|
||||
container = node
|
||||
|
||||
return initialize().then(function() {
|
||||
ReactDOM.render(<Layout {...controller.getState()} />, container)
|
||||
return controller.start(update)
|
||||
})
|
||||
}
|
||||
|
||||
const isMounted = function() {
|
||||
return !!container
|
||||
}
|
||||
|
||||
const update = function(props) {
|
||||
ReactDOM.render(<Layout {...props} />, container)
|
||||
}
|
||||
|
||||
const reload = function() {
|
||||
controller.load()
|
||||
}
|
||||
|
||||
const unmount = function() {
|
||||
if (isMounted()) {
|
||||
controller.stop()
|
||||
ReactDOM.unmountComponentAtNode(container)
|
||||
container = undefined
|
||||
}
|
||||
}
|
||||
|
||||
exports.configure = configure
|
||||
exports.mount = mount
|
||||
exports.isMounted = isMounted
|
||||
exports.update = update
|
||||
exports.reload = reload
|
||||
exports.unmount = unmount
|
||||
|
||||
export default exports
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (C) 2014 - present Instructure, Inc.
|
||||
* Copyright (C) 2021 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
|
@ -16,8 +16,7 @@
|
|||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
var fs = require('fs');
|
||||
import CoreDispatcher from '../../shared/core/dispatcher'
|
||||
import config from '../config'
|
||||
|
||||
module.exports = function readJSON(filePath) {
|
||||
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
||||
};
|
||||
export default new CoreDispatcher(config)
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* Copyright (C) 2021 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas 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, version 3 of the License.
|
||||
*
|
||||
* Canvas 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/>.
|
||||
*/
|
||||
|
||||
import config from './config'
|
||||
import delegate from './core/delegate'
|
||||
|
||||
export const configure = delegate.configure
|
||||
export const mount = delegate.mount
|
||||
export const isMounted = delegate.isMounted
|
||||
export const update = delegate.update
|
||||
export const reload = delegate.reload
|
||||
export const unmount = delegate.unmount
|
||||
export {config}
|
|
@ -0,0 +1,220 @@
|
|||
/*
|
||||
* Copyright (C) 2021 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas 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, version 3 of the License.
|
||||
*
|
||||
* Canvas 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/>.
|
||||
*/
|
||||
|
||||
import _ from 'lodash.underscore'
|
||||
|
||||
const find = _.find
|
||||
const RE_EXTRACT_LINK = /<([^>]+)>; rel="([^"]+)",?\s*/g
|
||||
const RE_EXTRACT_PP = /per_page=(\d+)/
|
||||
|
||||
// Extract pagination meta from a JSON-API payload inside the
|
||||
// "meta.pagination" set.
|
||||
const parseJsonApiPagination = function(respMeta, meta) {
|
||||
if (!meta) meta = {}
|
||||
|
||||
meta.perPage = respMeta.per_page
|
||||
meta.hasMore = !!respMeta.next
|
||||
meta.nextPage = meta.hasMore ? respMeta.page + 1 : undefined
|
||||
meta.count = respMeta.count
|
||||
|
||||
return meta
|
||||
}
|
||||
|
||||
// Extract pagination from the Link header.
|
||||
//
|
||||
// Here's a good reference:
|
||||
// https://developer.github.com/guides/traversing-with-pagination/
|
||||
const parseLinkPagination = function(linkHeader, meta) {
|
||||
let match
|
||||
const links = []
|
||||
|
||||
if (!meta) meta = {}
|
||||
|
||||
while ((match = RE_EXTRACT_LINK.exec(linkHeader))) {
|
||||
links.push({
|
||||
rel: match[2],
|
||||
href: match[1],
|
||||
page: parseInt(/page=(\d+)/.exec(match[1])[1], 10)
|
||||
})
|
||||
}
|
||||
|
||||
const nextLink = find(links, {rel: 'next'})
|
||||
const lastLink = find(links, {rel: 'last'})
|
||||
|
||||
meta.perPage = parseInt((RE_EXTRACT_PP.exec(linkHeader) || [])[1] || 0, 10)
|
||||
meta.hasMore = !!nextLink
|
||||
meta.nextPage = meta.hasMore ? nextLink.page : undefined
|
||||
|
||||
// Link header does not provide us with an accurate count of objects, so
|
||||
// we'll estimate it if we know how many we get per page, and we know the
|
||||
// index of the last page:
|
||||
if (lastLink) {
|
||||
meta.count = meta.perPage * lastLink.page
|
||||
}
|
||||
|
||||
return meta
|
||||
}
|
||||
|
||||
/**
|
||||
* @class Events.Mixins.PaginatedCollection
|
||||
* @extends {Backbone.Collection}
|
||||
*
|
||||
* Adds support for utilizing JSON-API pagination meta-data to allow fetching
|
||||
* any page of a paginated API resource, or all pages at once.
|
||||
*
|
||||
* Usage example:
|
||||
*
|
||||
* var Collection = Backbone.Collection.extend({
|
||||
* // install the mixin
|
||||
* constructor: function() {
|
||||
* PaginatedCollection(this);
|
||||
* return Backbone.Collection.apply(this, arguments);
|
||||
* },
|
||||
*
|
||||
* url: function() {
|
||||
* return '/users';
|
||||
* }
|
||||
* });
|
||||
*
|
||||
* var collection = new Collection();
|
||||
*
|
||||
* collection.fetch(); // /users
|
||||
* collection.length; // 10
|
||||
*
|
||||
* collection.fetchNext(); // /users?page=2
|
||||
* collection.length; // 20
|
||||
*
|
||||
* // load all available users in one go:
|
||||
* // /users?page=1
|
||||
* // ...
|
||||
* // /users?page=5
|
||||
* collection.fetchAll().then(function() {
|
||||
* collection.length; // 50
|
||||
* });
|
||||
*/
|
||||
const Mixin = {
|
||||
/**
|
||||
* Fetch the next page, if available.
|
||||
*
|
||||
* @param {Object} options
|
||||
* Normal options you'd pass to Backbone.Collection#fetch().
|
||||
*
|
||||
* @param {Number} [options.page]
|
||||
* If specified, exactly that page will be fetched, otherwise we'll
|
||||
* use the current cursor (or 1).
|
||||
*
|
||||
* @return {Promise}
|
||||
* Resolves when the page has been loaded and the pagination meta
|
||||
* parsed.
|
||||
*/
|
||||
fetchNext(options) {
|
||||
const meta = this._paginationMeta
|
||||
|
||||
if (!options) {
|
||||
options = {}
|
||||
} else if (options.hasOwnProperty('xhr')) {
|
||||
delete options.xhr
|
||||
}
|
||||
|
||||
if (!options.data) {
|
||||
options.data = {}
|
||||
}
|
||||
|
||||
options.data.page = options.page || meta.nextPage
|
||||
|
||||
options.success = function(payload, statusText, xhr) {
|
||||
const header = xhr.getResponseHeader('Link')
|
||||
|
||||
if (payload.meta && payload.meta.pagination) {
|
||||
parseJsonApiPagination(payload.meta.pagination, meta)
|
||||
} else if (header) {
|
||||
parseLinkPagination(header, meta)
|
||||
}
|
||||
|
||||
this.add(payload, {parse: true /* always parse */})
|
||||
}.bind(this)
|
||||
|
||||
return this.sync('read', this, options)
|
||||
},
|
||||
|
||||
/**
|
||||
* @return {Boolean}
|
||||
* Whether there's more data (that we know of) to pull in from the
|
||||
* server.
|
||||
*/
|
||||
canLoadMore() {
|
||||
return !!this._paginationMeta.hasMore
|
||||
},
|
||||
|
||||
/**
|
||||
* Fetch all available pages.
|
||||
*
|
||||
* @param {Object} options
|
||||
* Options to pass to #fetchNext. "page" is not allowed here and
|
||||
* will be ignored if specified.
|
||||
*
|
||||
* @return {Promise}
|
||||
* Resolves when *all* pages have been loaded.
|
||||
*/
|
||||
fetchAll(options) {
|
||||
const meta = this._paginationMeta
|
||||
|
||||
if (!options) {
|
||||
options = {}
|
||||
} else if (options.hasOwnProperty('page')) {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.error(
|
||||
'You should not specify a page when fetching all pages since it ' +
|
||||
'will be reset to 1!'
|
||||
)
|
||||
}
|
||||
|
||||
delete options.page
|
||||
}
|
||||
|
||||
if (options.reset) {
|
||||
this.reset(null, {silent: true})
|
||||
}
|
||||
|
||||
meta.nextPage = 1
|
||||
|
||||
return (function fetch(collection) {
|
||||
return collection.fetchNext(options).then(function() {
|
||||
if (meta.hasMore) {
|
||||
return fetch(collection)
|
||||
} else {
|
||||
return collection
|
||||
}
|
||||
})
|
||||
})(this)
|
||||
},
|
||||
|
||||
/** @private */
|
||||
_resetPaginationMeta() {
|
||||
this._paginationMeta = {}
|
||||
}
|
||||
}
|
||||
|
||||
export default function applyMixin(collection) {
|
||||
collection.fetchNext = Mixin.fetchNext
|
||||
collection.fetchAll = Mixin.fetchAll
|
||||
collection._resetPaginationMeta = Mixin._resetPaginationMeta
|
||||
|
||||
collection.on('reset', collection._resetPaginationMeta, collection)
|
||||
collection._resetPaginationMeta()
|
||||
}
|
|
@ -0,0 +1,125 @@
|
|||
/*
|
||||
* Copyright (C) 2021 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas 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, version 3 of the License.
|
||||
*
|
||||
* Canvas 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/>.
|
||||
*/
|
||||
|
||||
import Subject from '../question_answered_event_decorator';
|
||||
import Backbone from 'backbone';
|
||||
import { findWhere } from 'lodash'
|
||||
|
||||
describe('Models.QuestionAnsweredEventDecorator', () => {
|
||||
describe('#decorateAnswerRecord', () => {
|
||||
describe('inferring whether a question is answered', () => {
|
||||
var record = {};
|
||||
var questionType;
|
||||
var subject = function(answer) {
|
||||
record.answer = answer;
|
||||
|
||||
return Subject.decorateAnswerRecord({
|
||||
questionType: questionType
|
||||
}, record);
|
||||
};
|
||||
|
||||
it('multiple_choice_question and many friends (scalar answers)', function() {
|
||||
questionType = 'multiple_choice_question';
|
||||
|
||||
subject(null);
|
||||
expect(record.answered).toEqual(false);
|
||||
|
||||
subject('123');
|
||||
expect(record.answered).toEqual(true);
|
||||
});
|
||||
|
||||
it('fill_in_multiple_blanks_question, multiple_dropdowns', function() {
|
||||
questionType = 'fill_in_multiple_blanks_question';
|
||||
|
||||
subject({ color1: null, color2: null });
|
||||
expect(record.answered).toEqual(false, 'should be false when all blanks are nulls');
|
||||
|
||||
subject({ color1: 'something', color2: null });
|
||||
expect(record.answered).toEqual(true, 'should be true if any blank is filled with anything');
|
||||
});
|
||||
|
||||
it('matching_question', function() {
|
||||
questionType = 'matching_question';
|
||||
|
||||
subject([]);
|
||||
expect(record.answered).toEqual(false);
|
||||
|
||||
subject(null);
|
||||
expect(record.answered).toEqual(false);
|
||||
|
||||
subject([{ answer_id: '123', match_id: null }]);
|
||||
expect(record.answered).toEqual(false);
|
||||
|
||||
subject([{ answer_id: '123', match_id: '456' }]);
|
||||
expect(record.answered).toEqual(true);
|
||||
});
|
||||
|
||||
it('multiple_answers, file_upload', function() {
|
||||
questionType = 'matching_question';
|
||||
|
||||
subject([]);
|
||||
expect(record.answered).toEqual(false);
|
||||
|
||||
subject(null);
|
||||
expect(record.answered).toEqual(false);
|
||||
|
||||
subject(null);
|
||||
expect(record.answered).toEqual(false);
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#run', function() {
|
||||
it('should mark latest answers to all questions', function() {
|
||||
var events = [
|
||||
{
|
||||
data: [
|
||||
{ quizQuestionId: '1', answer: 'something' },
|
||||
{ quizQuestionId: '2', answer: null }
|
||||
]
|
||||
},
|
||||
{
|
||||
data: [
|
||||
{ quizQuestionId: '1', answer: 'something else' }
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
var eventCollection = events.map(function(attrs) {
|
||||
return new Backbone.Model(attrs);
|
||||
});
|
||||
|
||||
var questions = [
|
||||
{ id: '1' },
|
||||
{ id: '2' }
|
||||
];
|
||||
|
||||
var findQuestionRecord = (eventIndex, id) => (
|
||||
eventCollection[eventIndex].get('data').find(x => x.quizQuestionId === id)
|
||||
);
|
||||
|
||||
Subject.run(eventCollection, questions);
|
||||
|
||||
expect(findQuestionRecord(0, '1').last).toBeFalsy();
|
||||
expect(findQuestionRecord(1, '1').last).toBeTruthy();
|
||||
|
||||
expect(findQuestionRecord(0, '2').last).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* Copyright (C) 2021 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas 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, version 3 of the License.
|
||||
*
|
||||
* Canvas 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/>.
|
||||
*/
|
||||
|
||||
import Backbone from 'backbone'
|
||||
import pickAndNormalize from '../../shared/models/common/pick_and_normalize'
|
||||
import fromJSONAPI from '../../shared/models/common/from_jsonapi'
|
||||
import K from '../constants'
|
||||
|
||||
const QuizSubmissionEvent = Backbone.Model.extend({
|
||||
parse(payload) {
|
||||
let attrs
|
||||
|
||||
attrs = fromJSONAPI(payload, 'quiz_submission_events', true)
|
||||
attrs = pickAndNormalize(attrs, K.EVENT_ATTRS)
|
||||
attrs.type = attrs.eventType
|
||||
attrs.data = attrs.eventData
|
||||
|
||||
delete attrs.eventType
|
||||
delete attrs.eventData
|
||||
|
||||
if (attrs.type === K.EVT_QUESTION_ANSWERED) {
|
||||
attrs.data = attrs.data.map(function(record) {
|
||||
return pickAndNormalize(record, K.EVENT_DATA_ATTRS)
|
||||
})
|
||||
}
|
||||
|
||||
if (attrs.type === K.EVT_PAGE_BLURRED) {
|
||||
attrs.flag = K.EVT_FLAG_WARNING
|
||||
} else if (attrs.type === K.EVT_PAGE_FOCUSED) {
|
||||
attrs.flag = K.EVT_FLAG_OK
|
||||
}
|
||||
|
||||
return attrs
|
||||
}
|
||||
})
|
||||
|
||||
export default QuizSubmissionEvent
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* Copyright (C) 2021 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas 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, version 3 of the License.
|
||||
*
|
||||
* Canvas 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/>.
|
||||
*/
|
||||
|
||||
import Backbone from 'backbone'
|
||||
import pickAndNormalize from '../../shared/models/common/pick_and_normalize'
|
||||
import fromJSONAPI from '../../shared/models/common/from_jsonapi'
|
||||
import K from '../constants'
|
||||
import inflections from '../../shared/util/inflections'
|
||||
|
||||
const camelize = inflections.camelize
|
||||
|
||||
export default Backbone.Model.extend({
|
||||
parse(payload) {
|
||||
let attrs
|
||||
|
||||
attrs = fromJSONAPI(payload, 'quiz_questions', true)
|
||||
attrs = pickAndNormalize(attrs, K.QUESTION_ATTRS)
|
||||
attrs.id = '' + attrs.id
|
||||
attrs.readableType = camelize(
|
||||
attrs.questionType.replace(/_question$/, '').replace(/_/g, ' '),
|
||||
false
|
||||
)
|
||||
|
||||
return attrs
|
||||
}
|
||||
})
|
|
@ -0,0 +1,135 @@
|
|||
/*
|
||||
* Copyright (C) 2021 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas 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, version 3 of the License.
|
||||
*
|
||||
* Canvas 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/>.
|
||||
*/
|
||||
|
||||
import _ from 'lodash.underscore'
|
||||
import K from '../constants'
|
||||
|
||||
const findWhere = _.findWhere
|
||||
const keys = Object.keys
|
||||
const QuestionAnsweredEventDecorator = {}
|
||||
|
||||
QuestionAnsweredEventDecorator.decorateAnswerRecord = function(question, record) {
|
||||
let answered = false
|
||||
const answer = record.answer
|
||||
let blank
|
||||
|
||||
switch (question.questionType) {
|
||||
case K.Q_NUMERICAL:
|
||||
case K.Q_CALCULATED:
|
||||
case K.Q_MULTIPLE_CHOICE:
|
||||
case K.Q_SHORT_ANSWER:
|
||||
case K.Q_ESSAY:
|
||||
answered = answer !== null
|
||||
break
|
||||
|
||||
case K.Q_FILL_IN_MULTIPLE_BLANKS:
|
||||
case K.Q_MULTIPLE_DROPDOWNS:
|
||||
for (blank in answer) {
|
||||
if (answer.hasOwnProperty(blank)) {
|
||||
answered = answer[blank] !== null
|
||||
}
|
||||
|
||||
if (answered) {
|
||||
break
|
||||
}
|
||||
}
|
||||
break
|
||||
|
||||
case K.Q_MATCHING:
|
||||
if (answer instanceof Array && answer.length > 0) {
|
||||
// watch out that at this point, the attributes are not normalized
|
||||
// and not camelCased:
|
||||
answered = answer.some(function(pair) {
|
||||
return pair.match_id !== null
|
||||
})
|
||||
}
|
||||
|
||||
break
|
||||
case K.Q_MULTIPLE_ANSWERS:
|
||||
case K.Q_FILE_UPLOAD:
|
||||
answered = answer instanceof Array && answer.length > 0
|
||||
break
|
||||
|
||||
default:
|
||||
answered = answer !== null
|
||||
}
|
||||
|
||||
record.answered = answered
|
||||
}
|
||||
|
||||
/**
|
||||
* Extend the raw event attributes as received from the API with some stuff
|
||||
* that we'll need when rendering the views.
|
||||
*
|
||||
* This "decoration" could be done once after the payload is received and it
|
||||
* is not necessary to re-perform them, unless the event answer data has been
|
||||
* mutated.
|
||||
*
|
||||
* The decorations are:
|
||||
*
|
||||
* 1. `answered`
|
||||
* This is applied on the answer records inside the model's "data" attr.
|
||||
* ---
|
||||
* A boolean indicating whether an answer is present. This
|
||||
* differs in semantics based on the question type and that's why we can't
|
||||
* simply test for "answer" to be null or "".
|
||||
*
|
||||
* 2. `last`
|
||||
* This is applied on the answer records inside the model's "data" attr.
|
||||
* ---
|
||||
* A boolean indicating whether this answer record is the final answer
|
||||
* provided to the referenced question.
|
||||
*
|
||||
* @param {Models.Event[]} events
|
||||
* An array of Event instances of type EVT_QUESTION_ANSWERED.
|
||||
*
|
||||
* @param {Object[]} questions
|
||||
* An array of question data; this must contain all the questions
|
||||
* referenced by the event set above.
|
||||
*
|
||||
* @return {null}
|
||||
* Nothing is returned as the decoration is done in-place on the model
|
||||
* attributes.
|
||||
*/
|
||||
QuestionAnsweredEventDecorator.run = function(events, questions) {
|
||||
let finalAnswerEvents = {}
|
||||
|
||||
events.forEach(function(event) {
|
||||
event.attributes.data.forEach(function(record) {
|
||||
const question = questions.filter(function(question) {
|
||||
return question.id === record.quizQuestionId
|
||||
})[0]
|
||||
|
||||
finalAnswerEvents[question.id] = event
|
||||
|
||||
QuestionAnsweredEventDecorator.decorateAnswerRecord(question, record)
|
||||
})
|
||||
})
|
||||
|
||||
keys(finalAnswerEvents).forEach(function(quizQuestionId) {
|
||||
const event = finalAnswerEvents[quizQuestionId]
|
||||
|
||||
findWhere(event.attributes.data, {
|
||||
quizQuestionId
|
||||
}).last = true
|
||||
})
|
||||
|
||||
finalAnswerEvents = null
|
||||
}
|
||||
|
||||
export default QuestionAnsweredEventDecorator
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (C) 2014 - present Instructure, Inc.
|
||||
* Copyright (C) 2021 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
|
@ -16,20 +16,23 @@
|
|||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
// Helper for use with sinon.server.respondWith() to save you from:
|
||||
//
|
||||
// - JSON.stringifying() the body
|
||||
// - adding JSON response Content-Type headers
|
||||
// - remembering whether headers or body go first!
|
||||
//
|
||||
this.xhrResponse = function(statusCode, body, headers) {
|
||||
if (!headers) {
|
||||
headers = {};
|
||||
}
|
||||
import Backbone from 'backbone'
|
||||
import pickAndNormalize from '../../shared/models/common/pick_and_normalize'
|
||||
import fromJSONAPI from '../../shared/models/common/from_jsonapi'
|
||||
import K from '../constants'
|
||||
import config from '../config'
|
||||
|
||||
if (!headers['Content-Type']) {
|
||||
headers['Content-Type'] = 'application/json';
|
||||
}
|
||||
export default Backbone.Model.extend({
|
||||
url() {
|
||||
return config.submissionUrl
|
||||
},
|
||||
|
||||
return [ statusCode, headers, JSON.stringify(body) ];
|
||||
};
|
||||
parse(payload) {
|
||||
let attrs
|
||||
|
||||
attrs = fromJSONAPI(payload, 'quiz_submissions', true)
|
||||
attrs = pickAndNormalize(attrs, K.SUBMISSION_ATTRS)
|
||||
|
||||
return attrs
|
||||
}
|
||||
})
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* Copyright (C) 2021 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas 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, version 3 of the License.
|
||||
*
|
||||
* Canvas 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/>.
|
||||
*/
|
||||
|
||||
import {act, render, fireEvent} from '@testing-library/react'
|
||||
import React from 'react'
|
||||
import { MemoryRouter } from 'react-router-dom'
|
||||
import AnswerMatrix from '../answer_matrix'
|
||||
import assertChange from 'chai-assert-change'
|
||||
|
||||
describe('canvas_quizzes/events/routes/answer_matrix', () => {
|
||||
it('renders', () => {
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<AnswerMatrix params={{}} query={{}} />
|
||||
</MemoryRouter>
|
||||
)
|
||||
})
|
||||
})
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* Copyright (C) 2021 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas 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, version 3 of the License.
|
||||
*
|
||||
* Canvas 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/>.
|
||||
*/
|
||||
|
||||
import {act, render, fireEvent} from '@testing-library/react'
|
||||
import React from 'react'
|
||||
import { MemoryRouter } from 'react-router-dom'
|
||||
import AppRoute from '../app'
|
||||
import assertChange from 'chai-assert-change'
|
||||
|
||||
describe('canvas_quizzes/events/AppRoute', () => {
|
||||
it('renders', () => {
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<AppRoute params={{ id: 1 }} query={{}} />
|
||||
</MemoryRouter>
|
||||
)
|
||||
})
|
||||
})
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* Copyright (C) 2021 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas 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, version 3 of the License.
|
||||
*
|
||||
* Canvas 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/>.
|
||||
*/
|
||||
|
||||
import {act, render, fireEvent} from '@testing-library/react'
|
||||
import React from 'react'
|
||||
import { MemoryRouter } from 'react-router-dom'
|
||||
import EventStreamRoute from '../event_stream'
|
||||
import assertChange from 'chai-assert-change'
|
||||
|
||||
describe('canvas_quizzes/events/routes/event_stream', () => {
|
||||
it('renders', () => {
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<EventStreamRoute />
|
||||
</MemoryRouter>
|
||||
)
|
||||
})
|
||||
})
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* Copyright (C) 2021 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas 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, version 3 of the License.
|
||||
*
|
||||
* Canvas 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/>.
|
||||
*/
|
||||
|
||||
import {act, render, fireEvent} from '@testing-library/react'
|
||||
import React from 'react'
|
||||
import { MemoryRouter } from 'react-router-dom'
|
||||
import QuestionRoute from '../question'
|
||||
import assertChange from 'chai-assert-change'
|
||||
|
||||
describe('canvas_quizzes/events/QuestionRoute', () => {
|
||||
it('renders', () => {
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<QuestionRoute questions={[]} query={{}} />
|
||||
</MemoryRouter>
|
||||
)
|
||||
})
|
||||
})
|
|
@ -1,6 +1,5 @@
|
|||
/** @jsx React.DOM */
|
||||
/*
|
||||
* Copyright (C) 2014 - present Instructure, Inc.
|
||||
* Copyright (C) 2021 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
|
@ -17,13 +16,23 @@
|
|||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
define(function(require) {
|
||||
var React = require('old_version_of_react_used_by_canvas_quizzes_client_apps');
|
||||
var I18n = require('i18n!quiz_log_auditing').default;
|
||||
import React from 'react'
|
||||
import AnswerMatrix from '../views/answer_matrix'
|
||||
import Config from '../config'
|
||||
|
||||
const AnswerMatrixRoute = props => {
|
||||
if (!Config.allowMatrixView) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<em className="ic-QuestionInspector__NoAnswer">
|
||||
{I18n.t('no_answer', 'No answer')}
|
||||
</em>
|
||||
);
|
||||
});
|
||||
<AnswerMatrix
|
||||
loading={props.isLoading}
|
||||
questions={props.questions}
|
||||
events={props.events}
|
||||
submission={props.submission}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default AnswerMatrixRoute
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* Copyright (C) 2021 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas 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, version 3 of the License.
|
||||
*
|
||||
* Canvas 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/>.
|
||||
*/
|
||||
|
||||
import React from 'react'
|
||||
import Actions from '../actions'
|
||||
import Query from './query'
|
||||
|
||||
class AppRoute extends React.Component {
|
||||
componentDidUpdate() {
|
||||
if (this.props.query.attempt) {
|
||||
Actions.setActiveAttempt(this.props.query.attempt)
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div id="ic-QuizInspector">{this.props.children}</div>
|
||||
}
|
||||
}
|
||||
|
||||
export default props => (
|
||||
<Query>
|
||||
<AppRoute {...props} />
|
||||
</Query>
|
||||
)
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* Copyright (C) 2021 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas 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, version 3 of the License.
|
||||
*
|
||||
* Canvas 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/>.
|
||||
*/
|
||||
|
||||
import React from 'react'
|
||||
import EventStream from '../views/event_stream'
|
||||
import Session from '../views/session'
|
||||
|
||||
const EventStreamRoute = props => (
|
||||
<div>
|
||||
<Session
|
||||
submission={props.submission}
|
||||
attempt={props.attempt}
|
||||
availableAttempts={props.availableAttempts}
|
||||
/>
|
||||
|
||||
<EventStream
|
||||
submission={props.submission}
|
||||
events={props.events}
|
||||
questions={props.questions}
|
||||
attempt={props.attempt}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
export default EventStreamRoute
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* Copyright (C) 2021 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas 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, version 3 of the License.
|
||||
*
|
||||
* Canvas 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/>.
|
||||
*/
|
||||
|
||||
import React from 'react'
|
||||
import { useLocation } from 'react-router-dom'
|
||||
|
||||
// A custom hook that builds on useLocation to parse
|
||||
// the query string for you.
|
||||
function useQuery() {
|
||||
const params = new URLSearchParams(useLocation().search)
|
||||
const query = {}
|
||||
|
||||
for (const key of params.keys()) {
|
||||
query[key] = params.get(key)
|
||||
}
|
||||
|
||||
return query
|
||||
}
|
||||
|
||||
export default function Query(props) {
|
||||
const query = useQuery()
|
||||
|
||||
return React.cloneElement(props.children, {query})
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* Copyright (C) 2021 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas 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, version 3 of the License.
|
||||
*
|
||||
* Canvas 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/>.
|
||||
*/
|
||||
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import {useParams} from 'react-router-dom'
|
||||
import QuestionInspector from '../views/question_inspector'
|
||||
import QuestionListing from '../views/question_listing'
|
||||
import Query from './query'
|
||||
|
||||
const QuestionRoute = props => {
|
||||
const {id: questionId} = useParams()
|
||||
const question = props.questions.find(question => {
|
||||
return question.id === questionId
|
||||
})
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div id="not_right_side">
|
||||
<div id="content-wrapper">
|
||||
<div id="content" role="main" className="container-fluid">
|
||||
<QuestionInspector
|
||||
loading={props.isLoading}
|
||||
question={question}
|
||||
currentEventId={props.query.event}
|
||||
inspectedQuestionId={questionId}
|
||||
events={props.events}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="right-side-wrapper">
|
||||
<aside id="right-side">
|
||||
<QuestionListing
|
||||
activeQuestionId={questionId}
|
||||
activeEventId={props.query.event}
|
||||
questions={props.questions}
|
||||
query={props.query}
|
||||
/>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
QuestionRoute.propTypes = {
|
||||
questions: PropTypes.array.isRequired
|
||||
}
|
||||
|
||||
export default props => (
|
||||
<Query>
|
||||
<QuestionRoute {...props} />
|
||||
</Query>
|
||||
)
|
|
@ -0,0 +1,178 @@
|
|||
/*
|
||||
* Copyright (C) 2021 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas 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, version 3 of the License.
|
||||
*
|
||||
* Canvas 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/>.
|
||||
*/
|
||||
|
||||
import _ from 'lodash.underscore'
|
||||
import Config from '../config'
|
||||
import Dispatcher from '../core/dispatcher'
|
||||
import Environment from '../../shared/core/environment'
|
||||
import EventCollection from '../collections/events'
|
||||
import K from '../constants'
|
||||
import QuestionAnsweredEventDecorator from '../models/question_answered_event_decorator'
|
||||
import QuestionCollection from '../collections/questions'
|
||||
import Store from '../../shared/core/store'
|
||||
import Submission from '../models/submission'
|
||||
|
||||
const range = _.range
|
||||
|
||||
export default new Store(
|
||||
'events',
|
||||
{
|
||||
getInitialState() {
|
||||
let attempt = Config.attempt
|
||||
const requestedAttempt = Environment.getQueryParameter('attempt')
|
||||
|
||||
if (requestedAttempt) {
|
||||
attempt = parseInt(requestedAttempt, 10)
|
||||
}
|
||||
|
||||
return {
|
||||
submission: new Submission(),
|
||||
events: new EventCollection(),
|
||||
questions: new QuestionCollection(),
|
||||
|
||||
loading: false,
|
||||
|
||||
attempt,
|
||||
|
||||
/**
|
||||
* @property {Integer} latestAttempt
|
||||
*
|
||||
* Not necessarily the current attempt of the submission we're using,
|
||||
* but instead the latest attempt available.
|
||||
*
|
||||
* @see #loadInitialData.
|
||||
*/
|
||||
latestAttempt: attempt
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Alright, we need to query the submission for the first time ignoring
|
||||
* any specified attempt index so that we can tell how many attempts there
|
||||
* are.
|
||||
*
|
||||
* The API does not expose that piece of information.
|
||||
*
|
||||
* This needs to be called at most once per submission during the lifetime
|
||||
* of the app.
|
||||
*/
|
||||
loadInitialData() {
|
||||
return this.state.submission.fetch().then(() => {
|
||||
const newState = {}
|
||||
const latestAttempt = this.state.submission.get('attempt')
|
||||
|
||||
if (!this.state.attempt || this.state.attempt > latestAttempt) {
|
||||
newState.attempt = latestAttempt
|
||||
}
|
||||
|
||||
newState.latestAttempt = latestAttempt
|
||||
|
||||
this.setState(newState)
|
||||
})
|
||||
},
|
||||
|
||||
load() {
|
||||
this.setState({loading: true})
|
||||
return this.loadSubmission()
|
||||
.then(this.loadQuestions.bind(this))
|
||||
.then(this.loadEvents.bind(this))
|
||||
.finally(() => {
|
||||
this.setState({loading: false})
|
||||
})
|
||||
},
|
||||
|
||||
loadSubmission() {
|
||||
let data
|
||||
|
||||
if (this.state.attempt) {
|
||||
data = {attempt: this.state.attempt}
|
||||
}
|
||||
|
||||
return this.state.submission.fetch({data})
|
||||
},
|
||||
|
||||
loadQuestions() {
|
||||
return this.state.questions.fetchAll({
|
||||
reset: true,
|
||||
data: {
|
||||
quiz_submission_id: this.state.submission.get('id'),
|
||||
quiz_submission_attempt: this.state.attempt
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
loadEvents() {
|
||||
const events = this.state.events
|
||||
const questions = this.getQuestions()
|
||||
|
||||
return events
|
||||
.fetchAll({
|
||||
reset: true,
|
||||
data: {
|
||||
attempt: this.state.attempt,
|
||||
per_page: 50
|
||||
}
|
||||
})
|
||||
.then(function decorateAnswerEvents(/* payload */) {
|
||||
const answerEvents = events.filter(function(model) {
|
||||
return model.get('type') === K.EVT_QUESTION_ANSWERED
|
||||
})
|
||||
|
||||
QuestionAnsweredEventDecorator.run(answerEvents, questions)
|
||||
|
||||
return events
|
||||
})
|
||||
},
|
||||
|
||||
isLoading() {
|
||||
return this.state.loading
|
||||
},
|
||||
|
||||
getAll() {
|
||||
return this.state.events.toJSON()
|
||||
},
|
||||
|
||||
getQuestions() {
|
||||
return this.state.questions.toJSON()
|
||||
},
|
||||
|
||||
getSubmission() {
|
||||
return this.state.submission.toJSON()
|
||||
},
|
||||
|
||||
getAttempt() {
|
||||
return this.state.attempt
|
||||
},
|
||||
|
||||
getAvailableAttempts() {
|
||||
return range(1, Math.max(1, (this.state.latestAttempt || 0) + 1))
|
||||
},
|
||||
|
||||
setActiveAttempt(_attempt) {
|
||||
const attempt = parseInt(_attempt, 10)
|
||||
|
||||
if (this.getAvailableAttempts().indexOf(attempt) === -1) {
|
||||
throw new Error("Invalid attempt '" + attempt + "'")
|
||||
} else if (this.state.attempt !== attempt) {
|
||||
this.state.attempt = attempt
|
||||
this.load()
|
||||
}
|
||||
}
|
||||
},
|
||||
Dispatcher
|
||||
)
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (C) 2014 - present Instructure, Inc.
|
||||
* Copyright (C) 2021 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
|
@ -16,25 +16,23 @@
|
|||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
define(function(require) {
|
||||
var $ = require('jquery');
|
||||
var Void = require('canvas_packages/jquery/instructure_date_and_time');
|
||||
import {act, render, fireEvent} from '@testing-library/react'
|
||||
import React from 'react'
|
||||
import { MemoryRouter } from 'react-router-dom'
|
||||
import QuestionListing from '../question_listing'
|
||||
import assertChange from 'chai-assert-change'
|
||||
import K from '../../constants'
|
||||
|
||||
var exports = {};
|
||||
|
||||
exports.friendlyDatetime = function(dateTime, perspective) {
|
||||
var muddledDateTime = dateTime;
|
||||
|
||||
if (muddledDateTime) {
|
||||
muddledDateTime.clone = function() {
|
||||
return new Date(muddledDateTime.getTime());
|
||||
};
|
||||
}
|
||||
|
||||
return $.friendlyDatetime(muddledDateTime, perspective);
|
||||
};
|
||||
|
||||
exports.fudgeDateForProfileTimezone = $.fudgeDateForProfileTimezone;
|
||||
|
||||
return exports;
|
||||
});
|
||||
describe('canvas_quizzes/events/views/question_listing', () => {
|
||||
it('renders', () => {
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<QuestionListing
|
||||
questions={[
|
||||
{ id: 'q1', questionType: K.Q_SHORT_ANSWER }
|
||||
]}
|
||||
/>
|
||||
</MemoryRouter>
|
||||
)
|
||||
})
|
||||
})
|
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* Copyright (C) 2021 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas 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, version 3 of the License.
|
||||
*
|
||||
* Canvas 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/>.
|
||||
*/
|
||||
|
||||
import {act, render, fireEvent} from '@testing-library/react'
|
||||
import React from 'react'
|
||||
import { MemoryRouter } from 'react-router-dom'
|
||||
import Session from '../session'
|
||||
import assertChange from 'chai-assert-change'
|
||||
import K from '../../constants'
|
||||
|
||||
describe('canvas_quizzes/events/views/session', () => {
|
||||
it('renders', () => {
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<Session />
|
||||
</MemoryRouter>
|
||||
)
|
||||
})
|
||||
|
||||
it('renders a link for every available attempt', () => {
|
||||
const { queryByTestId, getByTestId } = render(
|
||||
<MemoryRouter>
|
||||
<Session availableAttempts={[1,2,3]} attempt={2} />
|
||||
</MemoryRouter>
|
||||
)
|
||||
|
||||
expect(queryByTestId('attempt-1')).toBeTruthy()
|
||||
expect(getByTestId('attempt-1').nodeName).toBe('A')
|
||||
expect(queryByTestId('attempt-3')).toBeTruthy()
|
||||
expect(getByTestId('attempt-3').nodeName).toBe('A')
|
||||
expect(queryByTestId('current-attempt')).toBeTruthy()
|
||||
expect(getByTestId('current-attempt').nodeName).not.toBe('A')
|
||||
})
|
||||
})
|
|
@ -0,0 +1,134 @@
|
|||
/*
|
||||
* Copyright (C) 2021 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas 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, version 3 of the License.
|
||||
*
|
||||
* Canvas 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/>.
|
||||
*/
|
||||
|
||||
import {act, render, fireEvent} from '@testing-library/react'
|
||||
import React from 'react'
|
||||
import { MemoryRouter } from 'react-router-dom'
|
||||
import Cell from '../cell'
|
||||
import assertChange from 'chai-assert-change'
|
||||
import K from '../../../constants'
|
||||
|
||||
describe('canvas_quizzes/events/views/answer_matrix/cell', () => {
|
||||
it('renders', () => {
|
||||
render(
|
||||
<Cell
|
||||
question={{
|
||||
id: 'q1'
|
||||
}}
|
||||
|
||||
event={{
|
||||
data: [{ quizQuestionId: 'q1' }]
|
||||
}}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
||||
it('renders nothing if there is no event', () => {
|
||||
render(<Cell question={{ id: 'q1' }} />)
|
||||
})
|
||||
|
||||
describe('when not expanded', () => {
|
||||
const question = {
|
||||
question: { id: '1', questionType: 'multiple_choice_question' }
|
||||
}
|
||||
|
||||
it('shows an emblem for an empty answer', function() {
|
||||
const { getByTestId } = render(
|
||||
<Cell {...question}
|
||||
event={{ data: [{ quizQuestionId: '1', answer: null }] }}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(getByTestId('emblem').classList).toContain('is-empty')
|
||||
});
|
||||
|
||||
it('shows an emblem for an answer', function() {
|
||||
const { getByTestId } = render(
|
||||
<Cell {...question}
|
||||
event={{
|
||||
data: [
|
||||
{ quizQuestionId: '1', answer: '123', answered: true }
|
||||
]
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(getByTestId('emblem').classList).toContain('is-answered')
|
||||
});
|
||||
|
||||
it('shows an emblem for the last answer', function() {
|
||||
const { getByTestId } = render(
|
||||
<Cell {...question}
|
||||
event={{
|
||||
data: [
|
||||
{ quizQuestionId: '1', answer: '123', answered: true, last: true }
|
||||
]
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(getByTestId('emblem').classList).toContain('is-answered')
|
||||
expect(getByTestId('emblem').classList).toContain('is-last')
|
||||
});
|
||||
|
||||
it('shows nothing for no answer', function() {
|
||||
const { queryByTestId } = render(
|
||||
<Cell {...question} />
|
||||
);
|
||||
|
||||
expect(queryByTestId('emblem')).toBeFalsy()
|
||||
});
|
||||
})
|
||||
|
||||
it('expands and truncates', () => {
|
||||
render(
|
||||
<Cell
|
||||
expanded
|
||||
shouldTruncate
|
||||
maxVisibleChars={5}
|
||||
question={{
|
||||
id: 'q1',
|
||||
questionType: K.Q_SHORT_ANSWER
|
||||
}}
|
||||
|
||||
event={{
|
||||
data: [{ quizQuestionId: 'q1', answer: 'hello world' }]
|
||||
}}
|
||||
/>
|
||||
)
|
||||
|
||||
expect(document.body.textContent).toMatch('hello...')
|
||||
})
|
||||
|
||||
it('expands as json', () => {
|
||||
render(
|
||||
<Cell
|
||||
expanded
|
||||
shouldTruncate
|
||||
question={{
|
||||
id: 'q1',
|
||||
questionType: K.Q_MULTIPLE_CHOICE
|
||||
}}
|
||||
|
||||
event={{
|
||||
data: [{ quizQuestionId: 'q1' }]
|
||||
}}
|
||||
/>
|
||||
)
|
||||
})
|
||||
})
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* Copyright (C) 2021 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas 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, version 3 of the License.
|
||||
*
|
||||
* Canvas 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/>.
|
||||
*/
|
||||
|
||||
import {act, render, fireEvent} from '@testing-library/react'
|
||||
import React from 'react'
|
||||
import Emblem from '../emblem'
|
||||
import assertChange from 'chai-assert-change'
|
||||
|
||||
describe('canvas_quizzes/events/views/answer_matrix/emblem', () => {
|
||||
it('renders', () => {
|
||||
render(
|
||||
<Emblem />
|
||||
)
|
||||
})
|
||||
})
|
|
@ -0,0 +1,140 @@
|
|||
/*
|
||||
* Copyright (C) 2021 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas 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, version 3 of the License.
|
||||
*
|
||||
* Canvas 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/>.
|
||||
*/
|
||||
|
||||
import {act, render, fireEvent} from '@testing-library/react'
|
||||
import React from 'react'
|
||||
import { MemoryRouter } from 'react-router-dom'
|
||||
import AnswerMatrix from '../'
|
||||
import assertChange from 'chai-assert-change'
|
||||
import K from '../../../constants'
|
||||
|
||||
describe('canvas_quizzes/events/views/answer_matrix', () => {
|
||||
it('renders', () => {
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<AnswerMatrix />
|
||||
</MemoryRouter>
|
||||
)
|
||||
})
|
||||
|
||||
it('truncates', () => {
|
||||
const { getByText, getByTestId } = render(
|
||||
<MemoryRouter>
|
||||
<AnswerMatrix
|
||||
maxVisibleChars={5}
|
||||
questions={[
|
||||
{ id: 'q1', questionType: K.Q_SHORT_ANSWER }
|
||||
]}
|
||||
|
||||
events={[
|
||||
{
|
||||
id: 'e1',
|
||||
type: K.EVT_QUESTION_ANSWERED,
|
||||
createdAt: "2014-11-16T13:39:19Z",
|
||||
data: [{ quizQuestionId: 'q1', answer: 'hello world', answered: true }]
|
||||
}
|
||||
]}
|
||||
|
||||
submission={{
|
||||
"startedAt": "2014-11-16T13:37:19Z"
|
||||
}}
|
||||
/>
|
||||
</MemoryRouter>
|
||||
)
|
||||
|
||||
// we must expand it first
|
||||
fireEvent.click(getByTestId('event-toggler-e1'))
|
||||
|
||||
assertChange({
|
||||
fn: () => fireEvent.click( getByText('Truncate textual answers') ),
|
||||
of: () => getByTestId('cell-e1').textContent,
|
||||
from: 'hello world',
|
||||
to: 'hello...'
|
||||
})
|
||||
})
|
||||
|
||||
it('expands all events', () => {
|
||||
const { getByText, getByTestId } = render(
|
||||
<MemoryRouter>
|
||||
<AnswerMatrix
|
||||
maxVisibleChars={5}
|
||||
questions={[
|
||||
{ id: 'q1', questionType: K.Q_SHORT_ANSWER }
|
||||
]}
|
||||
|
||||
events={[
|
||||
{
|
||||
id: 'e1',
|
||||
type: K.EVT_QUESTION_ANSWERED,
|
||||
createdAt: "2014-11-16T13:39:19Z",
|
||||
data: [{ quizQuestionId: 'q1', answer: 'hello world', answered: true }]
|
||||
}
|
||||
]}
|
||||
|
||||
submission={{
|
||||
"startedAt": "2014-11-16T13:37:19Z"
|
||||
}}
|
||||
/>
|
||||
</MemoryRouter>
|
||||
)
|
||||
|
||||
assertChange({
|
||||
fn: () => fireEvent.click( getByText('Expand all answers') ),
|
||||
of: () => {
|
||||
try { return !!getByTestId('cell-e1') } catch (e) { return false }
|
||||
},
|
||||
from: false,
|
||||
to: true
|
||||
})
|
||||
})
|
||||
|
||||
it('inverts', () => {
|
||||
const { getByText, getByTestId } = render(
|
||||
<MemoryRouter>
|
||||
<AnswerMatrix
|
||||
maxVisibleChars={5}
|
||||
questions={[
|
||||
{ id: 'q1', questionType: K.Q_SHORT_ANSWER }
|
||||
]}
|
||||
|
||||
events={[
|
||||
{
|
||||
id: 'e1',
|
||||
type: K.EVT_QUESTION_ANSWERED,
|
||||
createdAt: "2014-11-16T13:39:19Z",
|
||||
data: [{ quizQuestionId: 'q1', answer: 'hello world', answered: true }]
|
||||
}
|
||||
]}
|
||||
|
||||
submission={{
|
||||
"startedAt": "2014-11-16T13:37:19Z"
|
||||
}}
|
||||
/>
|
||||
</MemoryRouter>
|
||||
)
|
||||
|
||||
assertChange({
|
||||
fn: () => fireEvent.click( getByText('Invert') ),
|
||||
of: () => {
|
||||
try { return !!getByTestId('question-toggler-q1') } catch (e) { return false }
|
||||
},
|
||||
from: false,
|
||||
to: true
|
||||
})
|
||||
})
|
||||
})
|
|
@ -0,0 +1,76 @@
|
|||
/*
|
||||
* Copyright (C) 2021 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas 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, version 3 of the License.
|
||||
*
|
||||
* Canvas 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/>.
|
||||
*/
|
||||
|
||||
import {act, render, fireEvent} from '@testing-library/react'
|
||||
import React from 'react'
|
||||
import InvertedTable from '../inverted_table'
|
||||
import assertChange from 'chai-assert-change'
|
||||
|
||||
describe('canvas_quizzes/events/views/answer_matrix/inverted_table', () => {
|
||||
it('renders', () => {
|
||||
render(
|
||||
<InvertedTable
|
||||
expandAll
|
||||
questions={[
|
||||
{ id: 'q1' }
|
||||
]}
|
||||
events={[
|
||||
{
|
||||
id: 'e1',
|
||||
type: 'question_answered',
|
||||
createdAt: "2014-11-16T13:39:19Z",
|
||||
data: [{"quizQuestionId":"q1","answer":null,"answered":false}]
|
||||
}
|
||||
]}
|
||||
|
||||
submission={{
|
||||
"startedAt": "2014-11-16T13:37:19Z"
|
||||
}}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
||||
it('expands a question when clicked', () => {
|
||||
const { getByTestId } = render(
|
||||
<InvertedTable
|
||||
questions={[
|
||||
{ id: 'q1' }
|
||||
]}
|
||||
events={[
|
||||
{
|
||||
id: 'e1',
|
||||
type: 'question_answered',
|
||||
createdAt: "2014-11-16T13:39:19Z",
|
||||
data: [{"quizQuestionId":"q1","answer":'ANSWER!',"answered":false}]
|
||||
}
|
||||
]}
|
||||
|
||||
submission={{
|
||||
"startedAt": "2014-11-16T13:37:19Z"
|
||||
}}
|
||||
/>
|
||||
)
|
||||
|
||||
assertChange({
|
||||
fn: () => fireEvent.click(getByTestId('question-toggler-q1')),
|
||||
of: () => !!document.body.textContent.match('ANSWER!'),
|
||||
from: false,
|
||||
to: true
|
||||
})
|
||||
})
|
||||
})
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* Copyright (C) 2021 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas 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, version 3 of the License.
|
||||
*
|
||||
* Canvas 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/>.
|
||||
*/
|
||||
|
||||
import {act, render, fireEvent} from '@testing-library/react'
|
||||
import React from 'react'
|
||||
import Legend from '../legend'
|
||||
import assertChange from 'chai-assert-change'
|
||||
|
||||
describe('canvas_quizzes/events/views/answer_matrix/legend', () => {
|
||||
it('renders', () => {
|
||||
render(
|
||||
<Legend />
|
||||
)
|
||||
})
|
||||
})
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* Copyright (C) 2021 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas 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, version 3 of the License.
|
||||
*
|
||||
* Canvas 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/>.
|
||||
*/
|
||||
|
||||
import {act, render, fireEvent} from '@testing-library/react'
|
||||
import React from 'react'
|
||||
import Option from '../option'
|
||||
import assertChange from 'chai-assert-change'
|
||||
|
||||
describe('canvas_quizzes/events/views/answer_matrix/option', () => {
|
||||
it('renders', () => {
|
||||
render(
|
||||
<Option />
|
||||
)
|
||||
})
|
||||
|
||||
it('emits onChange', () => {
|
||||
const onChange = jest.fn()
|
||||
const { getByTestId } = render(<Option onChange={onChange} />)
|
||||
|
||||
assertChange({
|
||||
fn: () => fireEvent.click(getByTestId('checkbox')),
|
||||
of: () => onChange.mock.calls.length,
|
||||
by: 1
|
||||
})
|
||||
})
|
||||
})
|
|
@ -0,0 +1,76 @@
|
|||
/*
|
||||
* Copyright (C) 2021 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas 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, version 3 of the License.
|
||||
*
|
||||
* Canvas 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/>.
|
||||
*/
|
||||
|
||||
import {act, render, fireEvent} from '@testing-library/react'
|
||||
import React from 'react'
|
||||
import Table from '../table'
|
||||
import assertChange from 'chai-assert-change'
|
||||
|
||||
describe('canvas_quizzes/events/views/answer_matrix/table', () => {
|
||||
it('renders', () => {
|
||||
render(
|
||||
<Table
|
||||
expandAll
|
||||
questions={[
|
||||
{ id: 'q1' }
|
||||
]}
|
||||
events={[
|
||||
{
|
||||
id: 'e1',
|
||||
type: 'question_answered',
|
||||
createdAt: "2014-11-16T13:39:19Z",
|
||||
data: [{"quizQuestionId":"q1","answer":null,"answered":false}]
|
||||
}
|
||||
]}
|
||||
|
||||
submission={{
|
||||
"startedAt": "2014-11-16T13:37:19Z"
|
||||
}}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
||||
it('expands a question when clicked', () => {
|
||||
const { getByTestId } = render(
|
||||
<Table
|
||||
questions={[
|
||||
{ id: 'q1' }
|
||||
]}
|
||||
events={[
|
||||
{
|
||||
id: 'e1',
|
||||
type: 'question_answered',
|
||||
createdAt: "2014-11-16T13:39:19Z",
|
||||
data: [{"quizQuestionId":"q1","answer":'ANSWER!',"answered":false}]
|
||||
}
|
||||
]}
|
||||
|
||||
submission={{
|
||||
"startedAt": "2014-11-16T13:37:19Z"
|
||||
}}
|
||||
/>
|
||||
)
|
||||
|
||||
assertChange({
|
||||
fn: () => fireEvent.click(getByTestId('event-toggler-e1')),
|
||||
of: () => !!document.body.textContent.match('ANSWER!'),
|
||||
from: false,
|
||||
to: true
|
||||
})
|
||||
})
|
||||
})
|
|
@ -0,0 +1,80 @@
|
|||
/*
|
||||
* Copyright (C) 2021 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas 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, version 3 of the License.
|
||||
*
|
||||
* Canvas 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/>.
|
||||
*/
|
||||
|
||||
import React from 'react'
|
||||
import K from '../../constants'
|
||||
import Emblem from './emblem'
|
||||
|
||||
// These questions types will have their answer cells truncated if it goes
|
||||
// over the character visibility threshold:
|
||||
const FREE_FORM_QUESTION_TYPES = [K.Q_ESSAY, K.Q_SHORT_ANSWER]
|
||||
|
||||
/**
|
||||
* @class Cell
|
||||
* @memberOf Views.AnswerMatrix
|
||||
*
|
||||
* A table cell that renders an answer to a question, based on the question
|
||||
* type, the table options, and other things.
|
||||
*/
|
||||
const Cell = props => {
|
||||
let formattedAnswer, answerSz, encodeAsJson
|
||||
const record = props.event.data.find(x => x.quizQuestionId === props.question.id)
|
||||
|
||||
if (!record) {
|
||||
return null
|
||||
}
|
||||
|
||||
formattedAnswer = record.answer
|
||||
encodeAsJson = true
|
||||
|
||||
// show the answer only if the expandAll option is turned on, or the
|
||||
// current event is activated (i.e, the row was clicked):
|
||||
if (props.expanded) {
|
||||
if (FREE_FORM_QUESTION_TYPES.indexOf(props.question.questionType) > -1) {
|
||||
encodeAsJson = false
|
||||
|
||||
if (props.shouldTruncate) {
|
||||
formattedAnswer = record.answer || ''
|
||||
answerSz = formattedAnswer.length
|
||||
|
||||
if (answerSz > props.maxVisibleChars) {
|
||||
formattedAnswer = formattedAnswer.substr(0, props.maxVisibleChars)
|
||||
formattedAnswer += '...'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<pre data-testid={`cell-${props.event.id}`}>
|
||||
{encodeAsJson ? JSON.stringify(formattedAnswer, null, 2) : formattedAnswer}
|
||||
</pre>
|
||||
)
|
||||
} else {
|
||||
return <Emblem {...record} />
|
||||
}
|
||||
}
|
||||
|
||||
Cell.defaultProps = {
|
||||
expanded: false,
|
||||
shouldTruncate: false,
|
||||
event: {data: []},
|
||||
question: {},
|
||||
maxVisibleChars: K.MAX_VISIBLE_CHARS
|
||||
}
|
||||
|
||||
export default Cell
|
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* Copyright (C) 2021 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas 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, version 3 of the License.
|
||||
*
|
||||
* Canvas 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/>.
|
||||
*/
|
||||
|
||||
import React from 'react'
|
||||
|
||||
/**
|
||||
* @class Events.Views.AnswerMatrix.Emblem
|
||||
*
|
||||
* Woop.
|
||||
*
|
||||
* @seed An emblem for an empty answer.
|
||||
* {}
|
||||
*
|
||||
* @seed An emblem for some answer.
|
||||
* { "answered": true }
|
||||
*
|
||||
* @seed An emblem for the final answer.
|
||||
* { "answered": true, "last": true }
|
||||
*/
|
||||
const Emblem = ({answered, last}) => {
|
||||
let className = 'ic-AnswerMatrix__Emblem'
|
||||
|
||||
if (answered && last) {
|
||||
className += ' is-answered is-last'
|
||||
} else if (answered) {
|
||||
className += ' is-answered'
|
||||
} else {
|
||||
className += ' is-empty'
|
||||
}
|
||||
|
||||
return <i data-testid="emblem" className={className} />
|
||||
}
|
||||
|
||||
export default Emblem
|
|
@ -0,0 +1,133 @@
|
|||
/*
|
||||
* Copyright (C) 2021 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas 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, version 3 of the License.
|
||||
*
|
||||
* Canvas 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/>.
|
||||
*/
|
||||
|
||||
import I18n from 'i18n!quiz_log_auditing.table_view'
|
||||
import InvertedTable from './inverted_table'
|
||||
import K from '../../constants'
|
||||
import Legend from './legend'
|
||||
import Option from './option'
|
||||
import React from 'react'
|
||||
import {Link} from 'react-router-dom'
|
||||
import Table from './table'
|
||||
|
||||
class AnswerMatrix extends React.Component {
|
||||
state = {
|
||||
activeEventId: null,
|
||||
shouldTruncate: false,
|
||||
expandAll: false,
|
||||
invert: false
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
questions: [],
|
||||
events: [],
|
||||
submission: {
|
||||
createdAt: new Date().toJSON()
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const events = this.props.events.filter(function(e) {
|
||||
return e.type === K.EVT_QUESTION_ANSWERED
|
||||
})
|
||||
|
||||
let className
|
||||
|
||||
if (this.state.expandAll) {
|
||||
className = 'expanded'
|
||||
}
|
||||
|
||||
return (
|
||||
<div data-testid="answer-matrix" id="ic-AnswerMatrix" className={className}>
|
||||
<h1 className="ic-QuizInspector__Header">
|
||||
{I18n.t('Answer Sequence')}
|
||||
|
||||
<div className="ic-QuizInspector__HeaderControls">
|
||||
<Option
|
||||
onChange={this.setOption.bind(this)}
|
||||
name="shouldTruncate"
|
||||
label={I18n.t('options.truncate', 'Truncate textual answers')}
|
||||
checked={this.state.shouldTruncate}
|
||||
/>
|
||||
|
||||
<Option
|
||||
onChange={this.setOption.bind(this)}
|
||||
name="expandAll"
|
||||
label={I18n.t('options.expand_all', 'Expand all answers')}
|
||||
checked={this.state.expandAll}
|
||||
/>
|
||||
|
||||
<Option
|
||||
onChange={this.setOption.bind(this)}
|
||||
name="invert"
|
||||
label={I18n.t('options.invert', 'Invert')}
|
||||
checked={this.state.invert}
|
||||
/>
|
||||
|
||||
<Link to={{pathname: '/', search: window.location.search}} className="btn btn-default">
|
||||
{I18n.t('buttons.go_to_stream', 'View Stream')}
|
||||
</Link>
|
||||
</div>
|
||||
</h1>
|
||||
|
||||
<Legend />
|
||||
|
||||
<div className="table-scroller">
|
||||
{this.state.invert ? this.renderInverted(events) : this.renderNormal(events)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
renderNormal(events) {
|
||||
return (
|
||||
<Table
|
||||
events={events}
|
||||
questions={this.props.questions}
|
||||
submission={this.props.submission}
|
||||
expandAll={this.state.expandAll}
|
||||
shouldTruncate={this.state.shouldTruncate}
|
||||
maxVisibleChars={this.props.maxVisibleChars}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
renderInverted(events) {
|
||||
return (
|
||||
<InvertedTable
|
||||
events={events}
|
||||
questions={this.props.questions}
|
||||
submission={this.props.submission}
|
||||
expandAll={this.state.expandAll}
|
||||
shouldTruncate={this.state.shouldTruncate}
|
||||
activeEventId={this.state.activeEventId}
|
||||
maxVisibleChars={this.props.maxVisibleChars}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
setOption(option, isChecked) {
|
||||
const newState = {}
|
||||
|
||||
newState[option] = isChecked
|
||||
|
||||
this.setState(newState)
|
||||
}
|
||||
}
|
||||
|
||||
export default AnswerMatrix
|
|
@ -0,0 +1,93 @@
|
|||
/*
|
||||
* Copyright (C) 2021 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas 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, version 3 of the License.
|
||||
*
|
||||
* Canvas 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/>.
|
||||
*/
|
||||
|
||||
import Cell from './cell'
|
||||
import I18n from 'i18n!quiz_log_auditing.inverted_table_view'
|
||||
import React from 'react'
|
||||
import secondsToTime from '../../../shared/util/seconds_to_time'
|
||||
|
||||
/**
|
||||
* @class Events.Views.AnswerMatrix.InvertedTable
|
||||
*
|
||||
* A table displaying the event series on the X axis, and the questions
|
||||
* on the Y axis. This table is optimal for inspecting answer contents, while
|
||||
* the "normal" table is optimized for viewing the answer sequence.
|
||||
*/
|
||||
class InvertedTable extends React.Component {
|
||||
state = {
|
||||
activeQuestionId: null
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<table className="ic-AnswerMatrix__Table ic-Table ic-Table--hover-row ic-Table--striped">
|
||||
<thead>
|
||||
<tr className="ic-Table__row--bg-neutral">
|
||||
<th key="question">{I18n.t('Question')}</th>
|
||||
|
||||
{this.props.events.map(this.renderHeaderCell.bind(this))}
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>{this.props.questions.map(this.renderContentRow.bind(this))}</tbody>
|
||||
</table>
|
||||
)
|
||||
}
|
||||
|
||||
renderHeaderCell(event) {
|
||||
const secondsSinceStart =
|
||||
(new Date(event.createdAt) - new Date(this.props.submission.startedAt)) / 1000
|
||||
|
||||
return <th key={'header-' + event.id}>{secondsToTime(secondsSinceStart)}</th>
|
||||
}
|
||||
|
||||
renderContentRow(question) {
|
||||
const expanded = this.props.expandAll || question.id === this.state.activeQuestionId
|
||||
const shouldTruncate = this.props.shouldTruncate
|
||||
|
||||
return (
|
||||
<tr
|
||||
key={'question-' + question.id}
|
||||
onClick={this.toggleAnswerVisibility.bind(this, question)}
|
||||
data-testid={`question-toggler-${question.id}`}
|
||||
>
|
||||
<td key="question">{question.id}</td>
|
||||
|
||||
{this.props.events.map(event => (
|
||||
<td key={['q', question.id, 'e', event.id].join('_')}>
|
||||
<Cell
|
||||
question={question}
|
||||
event={event}
|
||||
expanded={expanded}
|
||||
shouldTruncate={shouldTruncate}
|
||||
maxVisibleChars={this.props.maxVisibleChars}
|
||||
/>
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
)
|
||||
}
|
||||
|
||||
toggleAnswerVisibility(question) {
|
||||
this.setState(state => ({
|
||||
activeQuestionId: question.id === state.activeQuestionId ? null : question.id
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
export default InvertedTable
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* Copyright (C) 2021 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas 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, version 3 of the License.
|
||||
*
|
||||
* Canvas 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/>.
|
||||
*/
|
||||
|
||||
import Emblem from './emblem'
|
||||
import I18n from 'i18n!quiz_log_auditing.table_view'
|
||||
import React from 'react'
|
||||
|
||||
/**
|
||||
* @class Events.Views.AnswerMatrix.Legend
|
||||
*
|
||||
* A legend that explains what each type of "answer circle" denotes.
|
||||
*
|
||||
* @seed
|
||||
* {}
|
||||
*/
|
||||
const Legend = () => (
|
||||
<dl id="ic-AnswerMatrix__Legend">
|
||||
<dt>{I18n.t('legend.empty_circle', 'Empty Circle')}</dt>
|
||||
<dd>
|
||||
<Emblem />
|
||||
{I18n.t('legend.empty_circle_desc', 'An empty answer.')}
|
||||
</dd>
|
||||
|
||||
<dt>{I18n.t('legend.dotted_circle', 'Dotted Circle')}</dt>
|
||||
<dd>
|
||||
<Emblem answered />
|
||||
{I18n.t('legend.dotted_circle_desc', 'An answer, regardless of correctness.')}
|
||||
</dd>
|
||||
|
||||
<dt>{I18n.t('legend.filled_circle', 'Filled Circle')}</dt>
|
||||
<dd>
|
||||
<Emblem answered last />
|
||||
{I18n.t(
|
||||
'legend.filled_circle_desc',
|
||||
'The final answer for the question, the one that counts.'
|
||||
)}
|
||||
</dd>
|
||||
</dl>
|
||||
)
|
||||
|
||||
export default Legend
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (C) 2014 - present Instructure, Inc.
|
||||
* Copyright (C) 2021 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
|
@ -16,22 +16,23 @@
|
|||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
@import "/vendor/canvas_public/stylesheets_compiled/new_styles_normal_contrast/pages/g_vendor.css";
|
||||
@import "/vendor/canvas_public/stylesheets_compiled/new_styles_normal_contrast/base/c-common.css";
|
||||
@import "/dist/canvas_quizzes.css";
|
||||
import React from 'react'
|
||||
|
||||
.seed-data { display: none; }
|
||||
.seed-name {
|
||||
cursor: pointer;
|
||||
const Option = ({checked, label, name, onChange}) => {
|
||||
return (
|
||||
// it's a fluke since we are passing children for the label
|
||||
// eslint-disable-next-line jsx-a11y/label-has-associated-control
|
||||
<label>
|
||||
<input
|
||||
data-testid="checkbox"
|
||||
type="checkbox"
|
||||
onChange={e => onChange(name, e.target.checked)}
|
||||
checked={checked}
|
||||
/>
|
||||
|
||||
{label}
|
||||
</label>
|
||||
)
|
||||
}
|
||||
|
||||
.seed-runner {
|
||||
max-width: 100%;
|
||||
max-height: 320px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.seed-runner:not(:empty) {
|
||||
margin: 10px;
|
||||
margin-left: -9px;
|
||||
}
|
||||
export default Option
|
|
@ -0,0 +1,121 @@
|
|||
/*
|
||||
* Copyright (C) 2021 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas 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, version 3 of the License.
|
||||
*
|
||||
* Canvas 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/>.
|
||||
*/
|
||||
|
||||
import Cell from './cell'
|
||||
import I18n from 'i18n!quiz_log_auditing.table_view'
|
||||
import React from 'react'
|
||||
import secondsToTime from '../../../shared/util/seconds_to_time'
|
||||
|
||||
/**
|
||||
* @class Events.Views.AnswerMatrix.Table
|
||||
*
|
||||
* A table displaying the sequence of answers the student has provided to all
|
||||
* the questions. The answer cells will variate in shape based on the presence
|
||||
* of the answer and its position.
|
||||
*
|
||||
* @see Events.Views.AnswerMatrix.Emblem
|
||||
*
|
||||
* @seed A table of 8 questions and 25 events.
|
||||
* "apps/events/test/fixtures/loaded_table.json"
|
||||
*/
|
||||
class Table extends React.Component {
|
||||
state = {
|
||||
activeEventId: null
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
questions: [],
|
||||
events: [],
|
||||
submission: {}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<table className="ic-AnswerMatrix__Table ic-Table ic-Table--hover-row ic-Table--condensed">
|
||||
<thead>
|
||||
<tr className="ic-Table__row--bg-neutral">
|
||||
<th key="timestamp">
|
||||
<div>{I18n.t('headers.timestamp', 'Timestamp')}</div>
|
||||
</th>
|
||||
|
||||
{this.props.questions.map(this.renderHeaderCell.bind(this))}
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>{this.props.events.map(this.renderContentRow.bind(this))}</tbody>
|
||||
</table>
|
||||
)
|
||||
}
|
||||
|
||||
renderHeaderCell(question) {
|
||||
return (
|
||||
<th key={'question-' + question.id}>
|
||||
<div>
|
||||
{I18n.t('headers.question', 'Question %{position}', {
|
||||
position: question.position
|
||||
})}
|
||||
|
||||
<small>({question.id})</small>
|
||||
</div>
|
||||
</th>
|
||||
)
|
||||
}
|
||||
|
||||
renderContentRow(event) {
|
||||
let className
|
||||
const expanded = this.props.expandAll || event.id === this.state.activeEventId
|
||||
const shouldTruncate = this.props.shouldTruncate
|
||||
const secondsSinceStart =
|
||||
(new Date(event.createdAt) - new Date(this.props.submission.startedAt)) / 1000
|
||||
|
||||
if (this.props.activeEventId === event.id) {
|
||||
className = 'active'
|
||||
}
|
||||
|
||||
return (
|
||||
<tr
|
||||
key={'event-' + event.id}
|
||||
className={className}
|
||||
onClick={this.toggleAnswerVisibility.bind(this, event)}
|
||||
data-testid={`event-toggler-${event.id}`}
|
||||
>
|
||||
<td>{secondsToTime(secondsSinceStart)}</td>
|
||||
|
||||
{this.props.questions.map(question => (
|
||||
<td key={['q', question.id, 'e', event.id].join('_')}>
|
||||
<Cell
|
||||
question={question}
|
||||
event={event}
|
||||
expanded={expanded}
|
||||
shouldTruncate={shouldTruncate}
|
||||
maxVisibleChars={this.props.maxVisibleChars}
|
||||
/>
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
)
|
||||
}
|
||||
|
||||
toggleAnswerVisibility(event) {
|
||||
this.setState(state => ({
|
||||
activeEventId: event.id === state.activeEventId ? null : event.id
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
export default Table
|
|
@ -0,0 +1,114 @@
|
|||
/*
|
||||
* Copyright (C) 2021 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas 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, version 3 of the License.
|
||||
*
|
||||
* Canvas 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/>.
|
||||
*/
|
||||
|
||||
import {act, render, fireEvent} from '@testing-library/react'
|
||||
import React from 'react'
|
||||
import { MemoryRouter } from 'react-router-dom'
|
||||
import Event from '../event'
|
||||
import assertChange from 'chai-assert-change'
|
||||
import K from '../../../constants'
|
||||
|
||||
describe('canvas_quizzes/events/views/event_stream/event', () => {
|
||||
it('renders EVT_SESSION_STARTED', () => {
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<Event
|
||||
createdAt="2014-11-16T13:39:19Z"
|
||||
startedAt="2014-11-16T13:37:19Z"
|
||||
type={K.EVT_SESSION_STARTED}
|
||||
/>
|
||||
</MemoryRouter>
|
||||
)
|
||||
})
|
||||
|
||||
it('renders EVT_QUESTION_ANSWERED', () => {
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<Event
|
||||
createdAt="2014-11-16T13:39:19Z"
|
||||
startedAt="2014-11-16T13:37:19Z"
|
||||
type={K.EVT_QUESTION_ANSWERED}
|
||||
questions={[{ id: 'q1', questionType: K.Q_SHORT_ANSWER }]}
|
||||
data={[
|
||||
{ quizQuestionId: 'q1', answer: 'hello world', answered: true }
|
||||
]}
|
||||
/>
|
||||
</MemoryRouter>
|
||||
)
|
||||
})
|
||||
|
||||
it('renders EVT_QUESTION_VIEWED', () => {
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<Event
|
||||
createdAt="2014-11-16T13:39:19Z"
|
||||
startedAt="2014-11-16T13:37:19Z"
|
||||
type={K.EVT_QUESTION_VIEWED}
|
||||
questions={[{ id: 'q1', questionType: K.Q_SHORT_ANSWER }]}
|
||||
data={[
|
||||
{ quizQuestionId: 'q1', answer: 'hello world', answered: true }
|
||||
]}
|
||||
/>
|
||||
</MemoryRouter>
|
||||
)
|
||||
})
|
||||
|
||||
it('renders EVT_PAGE_BLURRED', () => {
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<Event
|
||||
createdAt="2014-11-16T13:39:19Z"
|
||||
startedAt="2014-11-16T13:37:19Z"
|
||||
type={K.EVT_QUESTION_VIEWED}
|
||||
questions={[]}
|
||||
data={[]}
|
||||
/>
|
||||
</MemoryRouter>
|
||||
)
|
||||
})
|
||||
|
||||
it('renders EVT_PAGE_FOCUSED', () => {
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<Event
|
||||
createdAt="2014-11-16T13:39:19Z"
|
||||
startedAt="2014-11-16T13:37:19Z"
|
||||
type={K.EVT_QUESTION_VIEWED}
|
||||
questions={[]}
|
||||
data={[]}
|
||||
/>
|
||||
</MemoryRouter>
|
||||
)
|
||||
})
|
||||
|
||||
it('renders EVT_QUESTION_FLAGGED', () => {
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<Event
|
||||
createdAt="2014-11-16T13:39:19Z"
|
||||
startedAt="2014-11-16T13:37:19Z"
|
||||
type={K.EVT_QUESTION_FLAGGED}
|
||||
questions={[{ id: 'q1', questionType: K.Q_SHORT_ANSWER }]}
|
||||
data={[
|
||||
{ quizQuestionId: 'q1', answer: 'hello world', answered: true }
|
||||
]}
|
||||
/>
|
||||
</MemoryRouter>
|
||||
)
|
||||
})
|
||||
})
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* Copyright (C) 2021 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas 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, version 3 of the License.
|
||||
*
|
||||
* Canvas 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/>.
|
||||
*/
|
||||
|
||||
import {act, render, fireEvent} from '@testing-library/react'
|
||||
import React from 'react'
|
||||
import { MemoryRouter } from 'react-router-dom'
|
||||
import EventStream from '../'
|
||||
import assertChange from 'chai-assert-change'
|
||||
import K from '../../../constants'
|
||||
|
||||
describe('canvas_quizzes/events/views/event_stream', () => {
|
||||
it('renders', () => {
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<EventStream
|
||||
events={[
|
||||
{
|
||||
id: 'e1',
|
||||
createdAt: "2014-11-16T13:39:19Z",
|
||||
startedAt: "2014-11-16T13:37:19Z",
|
||||
type: K.EVT_SESSION_STARTED,
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</MemoryRouter>
|
||||
)
|
||||
})
|
||||
})
|
|
@ -0,0 +1,187 @@
|
|||
/*
|
||||
* Copyright (C) 2021 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas 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, version 3 of the License.
|
||||
*
|
||||
* Canvas 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/>.
|
||||
*/
|
||||
|
||||
import classSet from '../../../shared/util/class_set'
|
||||
import I18n from 'i18n!quiz_log_auditing.event_stream'
|
||||
import K from '../../constants'
|
||||
import React from 'react'
|
||||
import secondsToTime from '../../../shared/util/seconds_to_time'
|
||||
import SightedUserContent from '../../../shared/components/sighted_user_content'
|
||||
import {Link} from 'react-router-dom'
|
||||
import {IconTroubleLine, IconCompleteLine, IconEmptyLine} from '@instructure/ui-icons'
|
||||
|
||||
class Event extends React.Component {
|
||||
static defaultProps = {
|
||||
startedAt: new Date()
|
||||
}
|
||||
|
||||
render() {
|
||||
const e = this.props
|
||||
const className = classSet({
|
||||
'ic-ActionLog__Entry': true,
|
||||
'is-warning': e.flag === K.EVT_FLAG_WARNING,
|
||||
'is-ok': e.flag === K.EVT_FLAG_OK,
|
||||
'is-neutral': !e.flag
|
||||
})
|
||||
return (
|
||||
<li className={className} key={'event-' + e.id}>
|
||||
{this.renderRow(e)}
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
||||
renderRow(e) {
|
||||
const secondsSinceStart = (new Date(e.createdAt) - new Date(e.startedAt)) / 1000
|
||||
|
||||
return (
|
||||
<div>
|
||||
<span className="ic-ActionLog__EntryTimestamp">{secondsToTime(secondsSinceStart)}</span>
|
||||
|
||||
<SightedUserContent className="ic-ActionLog__EntryFlag">
|
||||
{this.renderFlag(e.flag)}
|
||||
</SightedUserContent>
|
||||
|
||||
<div className="ic-ActionLog__EntryDescription">{this.renderDescription(e)}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
renderFlag(flag) {
|
||||
if (flag === K.EVT_FLAG_WARNING) {
|
||||
return <IconTroubleLine />
|
||||
} else if (flag === K.EVT_FLAG_OK) {
|
||||
return <IconCompleteLine />
|
||||
} else {
|
||||
return <IconEmptyLine />
|
||||
}
|
||||
}
|
||||
|
||||
renderDescription(event) {
|
||||
switch (event.type) {
|
||||
case K.EVT_SESSION_STARTED:
|
||||
return I18n.t('session_started', 'Session started')
|
||||
|
||||
case K.EVT_QUESTION_ANSWERED: {
|
||||
let valid_answers = event.data.filter(function(i) {
|
||||
return i.answer != null
|
||||
})
|
||||
|
||||
if (valid_answers.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{I18n.t(
|
||||
'question_answered',
|
||||
{
|
||||
one: 'Answered question:',
|
||||
other: 'Answered the following questions:'
|
||||
},
|
||||
{count: valid_answers.length}
|
||||
)}
|
||||
|
||||
<div className="ic-QuestionAnchors">
|
||||
{valid_answers.map(this.renderQuestionAnchor.bind(this))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
case K.EVT_QUESTION_VIEWED:
|
||||
return (
|
||||
<div>
|
||||
{I18n.t(
|
||||
'question_viewed',
|
||||
{
|
||||
one: 'Viewed (and possibly read) question',
|
||||
other: 'Viewed (and possibly read) the following questions:'
|
||||
},
|
||||
{count: event.data.length}
|
||||
)}
|
||||
|
||||
<div className="ic-QuestionAnchors">
|
||||
{event.data.map(this.renderQuestionAnchor.bind(this))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
case K.EVT_PAGE_BLURRED:
|
||||
return I18n.t('page_blurred', 'Stopped viewing the Canvas quiz-taking page...')
|
||||
|
||||
case K.EVT_PAGE_FOCUSED:
|
||||
return I18n.t('page_focused', 'Resumed.')
|
||||
|
||||
case K.EVT_QUESTION_FLAGGED: {
|
||||
let label
|
||||
|
||||
if (event.data.flagged) {
|
||||
label = I18n.t('question_flagged', 'Flagged question:')
|
||||
} else {
|
||||
label = I18n.t('question_unflagged', 'Unflagged question:')
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{label}
|
||||
|
||||
<div className="ic-QuestionAnchors">
|
||||
{this.renderQuestionAnchor(event.data.questionId)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
renderQuestionAnchor(record) {
|
||||
let id
|
||||
let question
|
||||
let position
|
||||
|
||||
if (typeof record === 'object') {
|
||||
id = record.quizQuestionId
|
||||
} else {
|
||||
id = record
|
||||
}
|
||||
|
||||
question = this.props.questions.find(q => {
|
||||
return q.id === id
|
||||
})
|
||||
|
||||
position = question && question.position
|
||||
|
||||
return (
|
||||
<Link
|
||||
key={'question-anchor' + id}
|
||||
to={{
|
||||
pathname: `/questions/${id}`,
|
||||
search: `?event=${this.props.id}&attempt=${this.props.attempt}`
|
||||
}}
|
||||
className="ic-QuestionAnchors__Anchor"
|
||||
>
|
||||
{'#' + position}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default Event
|
|
@ -0,0 +1,87 @@
|
|||
/*
|
||||
* Copyright (C) 2021 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas 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, version 3 of the License.
|
||||
*
|
||||
* Canvas 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/>.
|
||||
*/
|
||||
|
||||
import Event from './event'
|
||||
import I18n from 'i18n!quiz_log_auditing.event_stream'
|
||||
import K from '../../constants'
|
||||
import React from 'react'
|
||||
|
||||
const visibleEventTypes = [
|
||||
K.EVT_PAGE_BLURRED,
|
||||
K.EVT_PAGE_FOCUSED,
|
||||
K.EVT_QUESTION_ANSWERED,
|
||||
K.EVT_QUESTION_FLAGGED,
|
||||
K.EVT_QUESTION_VIEWED,
|
||||
K.EVT_SESSION_STARTED
|
||||
]
|
||||
|
||||
class EventStream extends React.Component {
|
||||
static defaultProps = {
|
||||
events: [],
|
||||
submission: {},
|
||||
questions: []
|
||||
}
|
||||
|
||||
render() {
|
||||
const visibleEvents = this.getVisibleEvents(this.props.events)
|
||||
|
||||
return (
|
||||
<div data-testid="event-stream" id="ic-EventStream">
|
||||
<h2>{I18n.t('headers.action_log', 'Action Log')}</h2>
|
||||
|
||||
{visibleEvents.length === 0 && (
|
||||
<p>
|
||||
{I18n.t(
|
||||
'notices.no_events_available',
|
||||
'There were no events logged during the quiz-taking session.'
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<ol id="ic-EventStream__ActionLog">{visibleEvents.map(this.renderEvent.bind(this))}</ol>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
renderEvent(e) {
|
||||
const props = {
|
||||
...e,
|
||||
startedAt: this.props.submission.startedAt,
|
||||
questions: this.props.questions,
|
||||
attempt: this.props.attempt
|
||||
}
|
||||
|
||||
return <Event key={e.id} {...props} />
|
||||
}
|
||||
|
||||
getVisibleEvents(events) {
|
||||
return events.filter(function(e) {
|
||||
if (visibleEventTypes.indexOf(e.type) === -1) {
|
||||
return false
|
||||
}
|
||||
if (e.type !== K.EVT_QUESTION_ANSWERED) {
|
||||
return true
|
||||
}
|
||||
return e.data.some(i => {
|
||||
return i.answer != null
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default EventStream
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* Copyright (C) 2021 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas 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, version 3 of the License.
|
||||
*
|
||||
* Canvas 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/>.
|
||||
*/
|
||||
|
||||
import Essay from './answers/essay'
|
||||
import FIMB from './answers/fill_in_multiple_blanks'
|
||||
import Matching from './answers/matching'
|
||||
import MultipleAnswers from './answers/multiple_answers'
|
||||
import MultipleChoice from './answers/multiple_choice'
|
||||
import MultipleDropdowns from './answers/multiple_dropdowns'
|
||||
import React from 'react'
|
||||
|
||||
const GenericRenderer = props => <div>{'' + props.answer}</div>
|
||||
const Renderers = [Essay, FIMB, Matching, MultipleAnswers, MultipleChoice, MultipleDropdowns]
|
||||
|
||||
const getRenderer = questionType => (
|
||||
Renderers.find(entry => entry.questionTypes.includes(questionType)) || GenericRenderer
|
||||
)
|
||||
|
||||
const Answer = props => {
|
||||
const Renderer = getRenderer(props.question.questionType)
|
||||
|
||||
return <Renderer {...props} />
|
||||
}
|
||||
|
||||
export default Answer
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* Copyright (C) 2021 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas 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, version 3 of the License.
|
||||
*
|
||||
* Canvas 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/>.
|
||||
*/
|
||||
|
||||
import {act, render, fireEvent} from '@testing-library/react'
|
||||
import React from 'react'
|
||||
import Essay from '../essay'
|
||||
import assertChange from 'chai-assert-change'
|
||||
|
||||
describe('canvas_quizzes/events/views/question_inspector/answers/essay', () => {
|
||||
it('renders', () => {
|
||||
render(<Essay answer="yea!" />)
|
||||
expect(document.body.textContent).toMatch('yea!')
|
||||
})
|
||||
|
||||
it('toggles between views', () => {
|
||||
const { getByText } = render(<Essay answer="<span id='custom-el'>yea!</span>" />)
|
||||
|
||||
assertChange({
|
||||
fn: () => fireEvent.click(getByText('View HTML')),
|
||||
of: () => !!document.getElementById('custom-el'),
|
||||
from: false,
|
||||
to: true
|
||||
})
|
||||
})
|
||||
})
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (C) 2014 - present Instructure, Inc.
|
||||
* Copyright (C) 2021 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
|
@ -16,12 +16,14 @@
|
|||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
// Creates a DOM element that ReactSuite tests will use to mount the subject
|
||||
// in. Although jasmine_react does that automatically on the start of each
|
||||
// ReactSuite, we will prepare it before-hand and expose it to jasmine.fixture
|
||||
// if you need to access directly.
|
||||
require([ 'jasmine_react' ], function(ReactSuite) {
|
||||
console.log('Preparing jasmine DOM fixture at `jasmine.fixture`');
|
||||
import {act, render, fireEvent} from '@testing-library/react'
|
||||
import React from 'react'
|
||||
import FillInMultipleBlanks from '../fill_in_multiple_blanks'
|
||||
import assertChange from 'chai-assert-change'
|
||||
|
||||
jasmine.fixture = ReactSuite.createDOMFixture();
|
||||
});
|
||||
describe('canvas_quizzes/events/views/question_inspector/answers/fill_in_multiple_blanks', () => {
|
||||
it('renders', () => {
|
||||
render(<FillInMultipleBlanks answer={{one: 'yea!'}} />)
|
||||
expect(document.body.textContent).toMatch('yea!')
|
||||
})
|
||||
})
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* Copyright (C) 2021 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas 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, version 3 of the License.
|
||||
*
|
||||
* Canvas 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/>.
|
||||
*/
|
||||
|
||||
import {act, render, fireEvent} from '@testing-library/react'
|
||||
import React from 'react'
|
||||
import Matching from '../matching'
|
||||
import assertChange from 'chai-assert-change'
|
||||
|
||||
describe('canvas_quizzes/events/views/question_inspector/answers/matching', () => {
|
||||
it('renders', () => {
|
||||
render(<Matching
|
||||
question={{
|
||||
answers: [{ id: 1, match_id: 2, left: '[match one]', right: 'nope' }],
|
||||
matches: [{ match_id: 2, text: '[did match]' }]
|
||||
}}
|
||||
answer={[{ answer_id: 1, match_id: 2 }]}
|
||||
/>)
|
||||
|
||||
expect(document.body.textContent).toMatch('[match one]')
|
||||
expect(document.body.textContent).toMatch('[did match]')
|
||||
})
|
||||
})
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* Copyright (C) 2021 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas 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, version 3 of the License.
|
||||
*
|
||||
* Canvas 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/>.
|
||||
*/
|
||||
|
||||
import {act, render, fireEvent} from '@testing-library/react'
|
||||
import React from 'react'
|
||||
import MultipleAnswers from '../multiple_answers'
|
||||
import assertChange from 'chai-assert-change'
|
||||
|
||||
describe('canvas_quizzes/events/views/question_inspector/answers/multiple_answers', () => {
|
||||
it('renders', () => {
|
||||
const { getByTestId } = render(<MultipleAnswers
|
||||
question={{
|
||||
answers: [
|
||||
{ id: 1, text: 'one' },
|
||||
{ id: 2, text: 'two' },
|
||||
{ id: 3, text: 'three' }
|
||||
],
|
||||
}}
|
||||
answer={['1', '3']}
|
||||
/>)
|
||||
|
||||
expect(getByTestId('answer-1').checked).toBe(true)
|
||||
expect(getByTestId('answer-2').checked).toBe(false)
|
||||
expect(getByTestId('answer-3').checked).toBe(true)
|
||||
})
|
||||
})
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* Copyright (C) 2021 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas 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, version 3 of the License.
|
||||
*
|
||||
* Canvas 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/>.
|
||||
*/
|
||||
|
||||
import {act, render, fireEvent} from '@testing-library/react'
|
||||
import React from 'react'
|
||||
import MultipleChoice from '../multiple_choice'
|
||||
import assertChange from 'chai-assert-change'
|
||||
|
||||
describe('canvas_quizzes/events/views/question_inspector/answers/multiple_choice', () => {
|
||||
it('renders', () => {
|
||||
const { getByTestId } = render(<MultipleChoice
|
||||
question={{
|
||||
answers: [
|
||||
{ id: 1, text: 'one' },
|
||||
{ id: 2, text: 'two' },
|
||||
{ id: 3, text: 'three' }
|
||||
],
|
||||
}}
|
||||
answer="1"
|
||||
/>)
|
||||
|
||||
expect(getByTestId('answer-1').checked).toBe(true)
|
||||
expect(getByTestId('answer-2').checked).toBe(false)
|
||||
expect(getByTestId('answer-3').checked).toBe(false)
|
||||
})
|
||||
})
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* Copyright (C) 2021 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas 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, version 3 of the License.
|
||||
*
|
||||
* Canvas 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/>.
|
||||
*/
|
||||
|
||||
import {act, render, fireEvent} from '@testing-library/react'
|
||||
import React from 'react'
|
||||
import MultipleDropdowns from '../multiple_dropdowns'
|
||||
import assertChange from 'chai-assert-change'
|
||||
|
||||
describe('canvas_quizzes/events/views/question_inspector/answers/multiple_dropdowns', () => {
|
||||
it('renders', () => {
|
||||
render(<MultipleDropdowns
|
||||
question={{
|
||||
answers: [
|
||||
{ id: 1, text: 'yea!' },
|
||||
{ id: 2, text: 'two' },
|
||||
{ id: 3, text: 'three' }
|
||||
],
|
||||
}}
|
||||
answer={{
|
||||
'one': '1'
|
||||
}}
|
||||
/>)
|
||||
|
||||
expect(document.body.textContent).toMatch('yea!')
|
||||
})
|
||||
})
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* Copyright (C) 2021 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas 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, version 3 of the License.
|
||||
*
|
||||
* Canvas 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/>.
|
||||
*/
|
||||
|
||||
import {act, render, fireEvent} from '@testing-library/react'
|
||||
import React from 'react'
|
||||
import NoAnswer from '../no_answer'
|
||||
import assertChange from 'chai-assert-change'
|
||||
|
||||
describe('canvas_quizzes/events/views/question_inspector/answers/no_answer', () => {
|
||||
it('renders', () => {
|
||||
render(<NoAnswer />)
|
||||
expect(document.body.textContent).toMatch('No answer')
|
||||
})
|
||||
})
|
|
@ -0,0 +1,62 @@
|
|||
/*
|
||||
* Copyright (C) 2021 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas 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, version 3 of the License.
|
||||
*
|
||||
* Canvas 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/>.
|
||||
*/
|
||||
|
||||
import Button from '../../../components/button'
|
||||
import I18n from 'i18n!quiz_log_auditing.question_answers.essay'
|
||||
import K from '../../../constants'
|
||||
import React from 'react'
|
||||
|
||||
class Essay extends React.Component {
|
||||
static defaultProps = {
|
||||
answer: ''
|
||||
}
|
||||
|
||||
state = {
|
||||
htmlView: false
|
||||
}
|
||||
|
||||
render() {
|
||||
let content
|
||||
|
||||
if (this.state.htmlView) {
|
||||
content = <div dangerouslySetInnerHTML={{__html: this.props.answer}} />
|
||||
} else {
|
||||
content = <pre>{this.props.answer}</pre>
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{content}
|
||||
|
||||
<Button type="default" onClick={this.toggleView.bind(this)}>
|
||||
{this.state.htmlView
|
||||
? I18n.t('view_plain_answer', 'View Plain')
|
||||
: I18n.t('view_html_answer', 'View HTML')}
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
toggleView() {
|
||||
this.setState(state => ({htmlView: !state.htmlView}))
|
||||
}
|
||||
}
|
||||
|
||||
Essay.questionTypes = [K.Q_ESSAY]
|
||||
|
||||
export default Essay
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (C) 2014 - present Instructure, Inc.
|
||||
* Copyright (C) 2021 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
|
@ -16,22 +16,25 @@
|
|||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
define(function(require) {
|
||||
var Dispatcher = require('./core/dispatcher');
|
||||
var EventStore = require('./stores/events');
|
||||
var Actions = {};
|
||||
import React from 'react'
|
||||
import K from '../../../constants'
|
||||
import NO_ANSWER from './no_answer'
|
||||
|
||||
Actions.dismissNotification = function(key) {
|
||||
return Dispatcher.dispatch('notifications:dismiss', key).promise;
|
||||
};
|
||||
const FIMB = ({ answer }) => (
|
||||
<table>
|
||||
<tbody>
|
||||
{Object.keys(answer).map(function(blank) {
|
||||
return (
|
||||
<tr key={'blank' + blank}>
|
||||
<th scope="row">{blank}</th>
|
||||
<td>{answer[blank] || NO_ANSWER}</td>
|
||||
</tr>
|
||||
)
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
)
|
||||
|
||||
Actions.reloadEvents = function() {
|
||||
EventStore.load();
|
||||
};
|
||||
FIMB.questionTypes = [K.Q_FILL_IN_MULTIPLE_BLANKS]
|
||||
|
||||
Actions.setActiveAttempt = function(attempt) {
|
||||
EventStore.setActiveAttempt(attempt);
|
||||
};
|
||||
|
||||
return Actions;
|
||||
});
|
||||
export default FIMB
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* Copyright (C) 2021 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas 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, version 3 of the License.
|
||||
*
|
||||
* Canvas 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/>.
|
||||
*/
|
||||
|
||||
import K from '../../../constants'
|
||||
import NO_ANSWER from './no_answer'
|
||||
import React from 'react'
|
||||
|
||||
const Matching = ({ answer, question }) => (
|
||||
<table>
|
||||
<tbody>
|
||||
{question.answers.map(questionAnswer => {
|
||||
let match
|
||||
const answerRecord = answer.find(
|
||||
record => String(record.answer_id) === String(questionAnswer.id)
|
||||
)
|
||||
|
||||
if (answerRecord) {
|
||||
match = question.matches.find(
|
||||
match => String(match.match_id) === String(answerRecord.match_id)
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<tr key={'answer-' + questionAnswer.id}>
|
||||
<th scope="col">{questionAnswer.left}</th>
|
||||
<td>{match ? match.text : NO_ANSWER}</td>
|
||||
</tr>
|
||||
)
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
)
|
||||
|
||||
Matching.questionTypes = [K.Q_MATCHING]
|
||||
|
||||
export default Matching
|
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
* Copyright (C) 2021 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas 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, version 3 of the License.
|
||||
*
|
||||
* Canvas 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/>.
|
||||
*/
|
||||
|
||||
import React from 'react'
|
||||
import K from '../../../constants'
|
||||
|
||||
class MultipleAnswers extends React.Component {
|
||||
static defaultProps = {
|
||||
answer: [],
|
||||
question: {answers: []}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="ic-QuestionInspector__MultipleAnswers">
|
||||
{this.props.question.answers.map(this.renderAnswer.bind(this))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
renderAnswer(answer) {
|
||||
const isSelected = this.props.answer.indexOf('' + answer.id) > -1
|
||||
|
||||
return (
|
||||
<div key={'answer' + answer.id}>
|
||||
<input
|
||||
data-testid={`answer-${answer.id}`}
|
||||
type="checkbox"
|
||||
readOnly
|
||||
disabled={!isSelected}
|
||||
checked={isSelected}
|
||||
/>
|
||||
|
||||
{answer.text}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
MultipleAnswers.questionTypes = [K.Q_MULTIPLE_ANSWERS]
|
||||
|
||||
export default MultipleAnswers
|
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
* Copyright (C) 2021 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas 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, version 3 of the License.
|
||||
*
|
||||
* Canvas 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/>.
|
||||
*/
|
||||
|
||||
import React from 'react'
|
||||
import K from '../../../constants'
|
||||
|
||||
class MultipleChoice extends React.Component {
|
||||
static defaultProps = {
|
||||
answer: [],
|
||||
question: {answers: []}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="ic-QuestionInspector__MultipleChoice">
|
||||
{this.props.question.answers.map(this.renderAnswer.bind(this))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
renderAnswer(answer) {
|
||||
const isSelected = this.props.answer.indexOf('' + answer.id) > -1
|
||||
|
||||
return (
|
||||
<div key={'answer' + answer.id}>
|
||||
<input
|
||||
data-testid={`answer-${answer.id}`}
|
||||
type="radio"
|
||||
readOnly
|
||||
disabled={!isSelected}
|
||||
checked={isSelected}
|
||||
/>
|
||||
|
||||
{answer.text}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
MultipleChoice.questionTypes = [K.Q_MULTIPLE_CHOICE, K.Q_TRUE_FALSE]
|
||||
|
||||
export default MultipleChoice
|
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* Copyright (C) 2021 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas 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, version 3 of the License.
|
||||
*
|
||||
* Canvas 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/>.
|
||||
*/
|
||||
|
||||
import React from 'react'
|
||||
import K from '../../../constants'
|
||||
import NO_ANSWER from './no_answer'
|
||||
|
||||
class MultipleDropdowns extends React.Component {
|
||||
render() {
|
||||
const {question} = this.props
|
||||
const studentAnswer = this.props.answer
|
||||
|
||||
return (
|
||||
<table>
|
||||
<tbody>
|
||||
{Object.keys(studentAnswer).map(blank => {
|
||||
const answerText =
|
||||
question.answers.find(answer => {
|
||||
return '' + answer.id === studentAnswer[blank]
|
||||
}) || {}
|
||||
|
||||
return (
|
||||
<tr key={'blank' + blank}>
|
||||
<th scope="row">{blank}</th>
|
||||
<td>{answerText.text || NO_ANSWER}</td>
|
||||
</tr>
|
||||
)
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
MultipleDropdowns.questionTypes = [K.Q_MULTIPLE_DROPDOWNS]
|
||||
|
||||
export default MultipleDropdowns
|
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* Copyright (C) 2021 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas 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, version 3 of the License.
|
||||
*
|
||||
* Canvas 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/>.
|
||||
*/
|
||||
|
||||
import React from 'react'
|
||||
import I18n from 'i18n!quiz_log_auditing'
|
||||
|
||||
export default () => (
|
||||
<em className="ic-QuestionInspector__NoAnswer">{I18n.t('no_answer', 'No answer')}</em>
|
||||
)
|
|
@ -0,0 +1,140 @@
|
|||
/*
|
||||
* Copyright (C) 2021 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas 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, version 3 of the License.
|
||||
*
|
||||
* Canvas 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/>.
|
||||
*/
|
||||
|
||||
import $ from 'jquery'
|
||||
import Answer from './answer'
|
||||
import classSet from '../../../shared/util/class_set'
|
||||
import I18n from 'i18n!quiz_log_auditing'
|
||||
import K from '../../constants'
|
||||
import NoAnswer from './answers/no_answer'
|
||||
import React from 'react'
|
||||
|
||||
class QuestionInspector extends React.Component {
|
||||
static defaultProps = {
|
||||
question: undefined,
|
||||
events: []
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
$('body').addClass('with-right-side')
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
$('body').removeClass('with-right-side')
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div id="ic-QuizInspector__QuestionInspector">
|
||||
{this.props.question && this.renderQuestion(this.props.question)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
renderQuestion(question) {
|
||||
const currentEventId = this.props.currentEventId
|
||||
const answers = []
|
||||
this.props.events
|
||||
.filter(function(event) {
|
||||
return (
|
||||
event.type === K.EVT_QUESTION_ANSWERED &&
|
||||
event.data.some(function(record) {
|
||||
return record.quizQuestionId === question.id
|
||||
})
|
||||
)
|
||||
})
|
||||
.sort(function(a, b) {
|
||||
return new Date(a.createdAt) - new Date(b.createdAt)
|
||||
})
|
||||
.forEach(function(event) {
|
||||
const records = event.data.filter(function(record) {
|
||||
return record.quizQuestionId === question.id
|
||||
})
|
||||
|
||||
records.forEach(function(record) {
|
||||
answers.push({
|
||||
active: event.id === currentEventId,
|
||||
value: record.answer,
|
||||
answered: record.answered
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1 className="ic-QuestionInspector__QuestionHeader">
|
||||
{I18n.t('question_header', 'Question #%{position}', {
|
||||
position: question.position
|
||||
})}
|
||||
|
||||
<span className="ic-QuestionInspector__QuestionType">
|
||||
{I18n.t('question_type', '%{type}', {type: question.readableType})}
|
||||
</span>
|
||||
|
||||
<span className="ic-QuestionInspector__QuestionId">(id: {question.id})</span>
|
||||
</h1>
|
||||
|
||||
<div
|
||||
className="ic-QuestionInspector__QuestionText"
|
||||
dangerouslySetInnerHTML={{__html: question.questionText}}
|
||||
/>
|
||||
|
||||
<hr />
|
||||
|
||||
<p>
|
||||
{I18n.t(
|
||||
'question_response_count',
|
||||
{
|
||||
zero: 'This question was never answered.',
|
||||
one: 'This question was answered once.',
|
||||
other: 'This question was answered %{count} times.'
|
||||
},
|
||||
{count: answers.length}
|
||||
)}
|
||||
</p>
|
||||
|
||||
<ol id="ic-QuestionInspector__Answers">{answers.map(this.renderAnswer.bind(this))}</ol>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
renderAnswer(record, index) {
|
||||
let answer
|
||||
const className = classSet({
|
||||
'ic-QuestionInspector__Answer': true,
|
||||
'ic-QuestionInspector__Answer--is-active': !!record.active
|
||||
})
|
||||
|
||||
if (record.answered) {
|
||||
answer = (
|
||||
<Answer
|
||||
key={'answer-' + index}
|
||||
answer={record.value}
|
||||
isActive={record.active}
|
||||
question={this.props.question}
|
||||
/>
|
||||
)
|
||||
} else {
|
||||
answer = <NoAnswer />
|
||||
}
|
||||
|
||||
return <li key={'answer-' + index} className={className}>{answer}</li>
|
||||
}
|
||||
}
|
||||
|
||||
export default QuestionInspector
|
|
@ -0,0 +1,76 @@
|
|||
/*
|
||||
* Copyright (C) 2021 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas 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, version 3 of the License.
|
||||
*
|
||||
* Canvas 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/>.
|
||||
*/
|
||||
|
||||
import I18n from 'i18n!quiz_log_auditing.navigation'
|
||||
import React from 'react'
|
||||
import {Link} from 'react-router-dom'
|
||||
import {IconArrowStartLine} from '@instructure/ui-icons'
|
||||
|
||||
class QuestionListing extends React.Component {
|
||||
static defaultProps = {
|
||||
questions: [],
|
||||
activeQuestionId: undefined,
|
||||
activeEventId: undefined
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<h2>{I18n.t('questions', 'Questions')}</h2>
|
||||
|
||||
<ol id="ic-QuizInspector__QuestionListing">
|
||||
{this.props.questions
|
||||
.sort(function(a, b) {
|
||||
return a.position > b.position
|
||||
})
|
||||
.map(this.renderQuestion.bind(this))}
|
||||
</ol>
|
||||
|
||||
<Link
|
||||
className="no-hover"
|
||||
to={{
|
||||
pathname: '/',
|
||||
search: window.location.search
|
||||
}}
|
||||
>
|
||||
<IconArrowStartLine /> {I18n.t('links.back_to_session_information', 'Back to Log')}
|
||||
</Link>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
renderQuestion(question) {
|
||||
return (
|
||||
<li key={question.id}>
|
||||
<Link
|
||||
className={this.props.activeQuestionId === question.id ? 'active' : undefined}
|
||||
to={{
|
||||
pathname: `/questions/${question.id}`,
|
||||
search: window.location.search
|
||||
}}
|
||||
>
|
||||
{I18n.t('links.question', 'Question %{position}', {
|
||||
position: question.position
|
||||
})}
|
||||
</Link>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default QuestionListing
|
|
@ -0,0 +1,141 @@
|
|||
/*
|
||||
* Copyright (C) 2021 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas 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, version 3 of the License.
|
||||
*
|
||||
* Canvas 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/>.
|
||||
*/
|
||||
|
||||
import Actions from '../actions'
|
||||
import Button from '../components/button'
|
||||
import Config from '../config'
|
||||
import I18n from 'i18n!quiz_log_auditing'
|
||||
import React from 'react'
|
||||
import ScreenReaderContent from '../../shared/components/screen_reader_content'
|
||||
import {IconRefreshLine} from '@instructure/ui-icons'
|
||||
import {Link} from 'react-router-dom'
|
||||
|
||||
class Session extends React.Component {
|
||||
static defaultProps = {
|
||||
submission: {},
|
||||
availableAttempts: []
|
||||
}
|
||||
|
||||
state = {
|
||||
accessibilityWarningFocused: false
|
||||
}
|
||||
|
||||
render() {
|
||||
let accessibilityWarningClasses = 'ic-QuizInspector__accessibility-warning'
|
||||
if (!this.state.accessibilityWarningFocused) {
|
||||
accessibilityWarningClasses += ' screenreader-only'
|
||||
}
|
||||
|
||||
const warningMessage = I18n.t(
|
||||
'links.log_accessibility_warning',
|
||||
'Warning: For improved accessibility when using Quiz Logs, please remain in the current Stream View.'
|
||||
)
|
||||
|
||||
return (
|
||||
<div id="ic-QuizInspector__Session">
|
||||
<div className="ic-QuizInspector__Header">
|
||||
<h1>{I18n.t('page_header', 'Session Information')}</h1>
|
||||
|
||||
<div className="ic-QuizInspector__HeaderControls">
|
||||
<Button onClick={Actions.reloadEvents}>
|
||||
<ScreenReaderContent>{I18n.t('buttons.reload_events', 'Reload')}</ScreenReaderContent>
|
||||
<IconRefreshLine />
|
||||
</Button>{' '}
|
||||
{Config.allowMatrixView && (
|
||||
<span>
|
||||
<span
|
||||
id="refreshButtonDescription"
|
||||
// eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
|
||||
tabIndex="0"
|
||||
className={accessibilityWarningClasses}
|
||||
onFocus={this.toggleViewable.bind(this)}
|
||||
onBlur={this.toggleViewable.bind(this)}
|
||||
aria-label={warningMessage}
|
||||
>
|
||||
{warningMessage}
|
||||
</span>
|
||||
<Link
|
||||
data-testid="view-table-button"
|
||||
to={{pathname: '/answer_matrix', search: window.location.search}}
|
||||
className="btn btn-default"
|
||||
aria-describedby="refreshButtonDescription"
|
||||
>
|
||||
{I18n.t('buttons.table_view', 'View Table')}
|
||||
</Link>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th scope="row">{I18n.t('session_table_headers.started_at', 'Started at')}</th>
|
||||
<td>{new Date(this.props.submission.startedAt).toString()}</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th scope="row">{I18n.t('session_table_headers.attempt', 'Attempt')}</th>
|
||||
<td>
|
||||
<div id="ic-QuizInspector__AttemptController">
|
||||
{this.props.availableAttempts.map(this.renderAttemptLink.bind(this))}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
renderAttemptLink(attempt) {
|
||||
let className = 'ic-AttemptController__Attempt'
|
||||
|
||||
if (attempt === this.props.attempt) {
|
||||
className += ' ic-AttemptController__Attempt--is-active'
|
||||
|
||||
return (
|
||||
<div data-testid="current-attempt" className={className} key={'attempt-' + attempt}>
|
||||
{attempt}
|
||||
</div>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<Link
|
||||
data-testid={`attempt-${attempt}`}
|
||||
key={attempt}
|
||||
to={{
|
||||
pathname: '/',
|
||||
search: `?attempt=${attempt}`
|
||||
}}
|
||||
className={className}
|
||||
>
|
||||
{attempt}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
toggleViewable() {
|
||||
this.setState(state => ({
|
||||
accessibilityWarningFocused: !state.accessibilityWarningFocused
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
export default Session
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* Copyright (C) 2021 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas 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, version 3 of the License.
|
||||
*
|
||||
* Canvas 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/>.
|
||||
*/
|
||||
|
||||
import {act, render, fireEvent} from '@testing-library/react'
|
||||
import React from 'react'
|
||||
import ScreenReaderContent from '../screen_reader_content'
|
||||
import assertChange from 'chai-assert-change'
|
||||
|
||||
describe('canvas_quizzes/components/screen_reader_content', () => {
|
||||
it('renders', () => {
|
||||
render(<ScreenReaderContent>yea!</ScreenReaderContent>)
|
||||
expect(document.body.textContent).toMatch('yea!')
|
||||
})
|
||||
|
||||
it('forces sentence delimiter', () => {
|
||||
render(<ScreenReaderContent forceSentenceDelimiter>yea!</ScreenReaderContent>)
|
||||
expect(document.body.textContent).toMatch('yea!')
|
||||
})
|
||||
|
||||
it('rejects html', () => {
|
||||
render(<ScreenReaderContent forceSentenceDelimiter dangerouslySetInnerHTML={{__html: '<span>yea!</span>'}} />)
|
||||
|
||||
expect(document.body.textContent).not.toMatch('yea!')
|
||||
})
|
||||
})
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (C) 2014 - present Instructure, Inc.
|
||||
* Copyright (C) 2021 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
|
@ -16,18 +16,14 @@
|
|||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
define(function(require) {
|
||||
var Backbone = require('canvas_packages/backbone');
|
||||
var pickAndNormalize = require('./common/pick_and_normalize');
|
||||
var K = require('../constants');
|
||||
import {act, render, fireEvent} from '@testing-library/react'
|
||||
import React from 'react'
|
||||
import SightedUserContent from '../sighted_user_content'
|
||||
import assertChange from 'chai-assert-change'
|
||||
|
||||
return Backbone.Model.extend({
|
||||
url: function() {
|
||||
return this.get('url');
|
||||
},
|
||||
|
||||
parse: function(payload) {
|
||||
return pickAndNormalize(payload, K.PROGRESS_ATTRS);
|
||||
},
|
||||
});
|
||||
});
|
||||
describe('canvas_quizzes/components/sighted_user_content', () => {
|
||||
it('renders', () => {
|
||||
render(<SightedUserContent>yea!</SightedUserContent>)
|
||||
expect(document.body.textContent).toMatch('yea!')
|
||||
})
|
||||
})
|
|
@ -0,0 +1,101 @@
|
|||
/*
|
||||
* Copyright (C) 2021 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas 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, version 3 of the License.
|
||||
*
|
||||
* Canvas 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/>.
|
||||
*/
|
||||
|
||||
import React, { forwardRef } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
/**
|
||||
* @class Components.ScreenReaderContent
|
||||
* @alternateClassName ScreenReaderContent
|
||||
*
|
||||
* A component that is only "visible" to screen-reader ATs. Sighted users
|
||||
* will not see nor be able to interact with instances of this component.
|
||||
*
|
||||
* See Components.SightedUserContent for the "counterpart" of this component,
|
||||
* although with less reliability.
|
||||
*
|
||||
*/
|
||||
const ScreenReaderContent = forwardRef((props, ref) => {
|
||||
const Tag = props.tagName || 'span'
|
||||
const tagProps = {}
|
||||
const customChildren = []
|
||||
|
||||
tagProps.className = 'screenreader-only'
|
||||
|
||||
if (props.forceSentenceDelimiter) {
|
||||
customChildren.push(generateSentenceDelimiter())
|
||||
}
|
||||
|
||||
if (customChildren.length) {
|
||||
// React disallows setting the @dangerouslySetInnerHTML prop and passing
|
||||
// children at the same time. So if the caller is attempting to pass
|
||||
// this prop and is also asking for enhancements that require custom
|
||||
// children such as @forceSentenceDelimiter then we cannot accomodate
|
||||
// the request and should notify them.
|
||||
//
|
||||
// The same effect could be achieved by setting that prop on a *child*
|
||||
// passed to the SRC component, e.g:
|
||||
//
|
||||
// <ScreenReaderContent forceSentenceDelimiter>
|
||||
// <span dangerouslySetInnerHTML={{__html: '<b>hi</b>'}} />
|
||||
// </ScreenReaderContent>
|
||||
//
|
||||
// // instead of:
|
||||
//
|
||||
// <ScreenReaderContent
|
||||
// forceSentenceDelimiter
|
||||
// dangerouslySetInnerHTML={{__html: '<b>hi</b>'}} />
|
||||
if (props.dangerouslySetInnerHTML) {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.error(`
|
||||
You are attempting to set the dangerouslySetInnerHTML prop
|
||||
on a ScreenReaderContent component, which prevents it from enabling
|
||||
further accessibility enhancements.
|
||||
|
||||
Try setting that property on a passed child instead.
|
||||
`)
|
||||
}
|
||||
} else {
|
||||
tagProps.children = [props.children, customChildren]
|
||||
}
|
||||
} else {
|
||||
// no custom children, pass children as-is:
|
||||
tagProps.children = props.children
|
||||
}
|
||||
|
||||
return <Tag ref={ref} {...tagProps}>{tagProps.children}</Tag>
|
||||
})
|
||||
|
||||
const generateSentenceDelimiter = () => (
|
||||
<em key="sentence-delimiter" role="presentation" aria-hidden>. </em>
|
||||
)
|
||||
|
||||
ScreenReaderContent.propTypes = {
|
||||
/**
|
||||
* @property {Boolean} [forceSentenceDelimiter=false]
|
||||
*
|
||||
* If you're passing in dynamic content and you're noticing that it's not
|
||||
* being read as a full sentence (e.g, some SRs are reading it along with
|
||||
* the next element), then you can try setting this property to true and
|
||||
* it will work a trick to make the SR pause after reading this element,
|
||||
* just as if it were a proper sentence.
|
||||
*/
|
||||
forceSentenceDelimiter: PropTypes.bool
|
||||
}
|
||||
|
||||
export default ScreenReaderContent
|
|
@ -0,0 +1,62 @@
|
|||
/*
|
||||
* Copyright (C) 2021 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas 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, version 3 of the License.
|
||||
*
|
||||
* Canvas 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/>.
|
||||
*/
|
||||
|
||||
import React from 'react'
|
||||
import cx from 'classnames'
|
||||
|
||||
/**
|
||||
* @class Components.SightedUserContent
|
||||
*
|
||||
* A component that *tries* to hide itself from screen-readers, absolutely
|
||||
* expecting that you're providing a more accessible version of the resource
|
||||
* using something like a ScreenReaderContent component.
|
||||
*
|
||||
* Be warned that this does not totally prevent all screen-readers from
|
||||
* seeing this content in all modes. For example, VoiceOver in OS X will
|
||||
* still see this element when running in the "Say-All" mode and read it
|
||||
* along with the accessible version you're providing.
|
||||
*
|
||||
* > **Warning**
|
||||
* >
|
||||
* > Use of this component is discouraged unless there's no alternative!!!
|
||||
* >
|
||||
* > The only one case that justifies its use is when design provides a
|
||||
* > totally inaccessible version of a resource, and you're trying to
|
||||
* > accommodate the design (for sighted users,) and provide a genuine layer
|
||||
* > of accessibility (for others.)
|
||||
*/
|
||||
const SightedUserContent = ({tagName: Tag = 'span', ...props}) => {
|
||||
return (
|
||||
<Tag
|
||||
{...props}
|
||||
// HTML5 [hidden] works in many screen-readers and in some cases, like
|
||||
// VoiceOver's Say-All mode, is the only thing that works for skipping
|
||||
// content. However, this clearly has the downside of hiding the
|
||||
// content from sighted users as well, so we resort to CSS to get the
|
||||
// items back into display and we win-win.
|
||||
hidden
|
||||
aria-hidden
|
||||
role="presentation"
|
||||
className={cx('sighted-user-content', props.className)}
|
||||
>
|
||||
{props.children}
|
||||
</Tag>
|
||||
)
|
||||
}
|
||||
|
||||
export default SightedUserContent
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* Copyright (C) 2021 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas 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, version 3 of the License.
|
||||
*
|
||||
* Canvas 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/>.
|
||||
*/
|
||||
|
||||
import React from 'react'
|
||||
|
||||
const Spinner = () => (
|
||||
<div className="ic-Spinner">
|
||||
<div className="rect1" />
|
||||
<div className="rect2" />
|
||||
<div className="rect3" />
|
||||
<div className="rect4" />
|
||||
<div className="rect5" />
|
||||
</div>
|
||||
)
|
||||
|
||||
export default Spinner
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (C) 2014 - present Instructure, Inc.
|
||||
* Copyright (C) 2021 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
|
@ -16,15 +16,12 @@
|
|||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
dist: {
|
||||
options: {
|
||||
style: 'expanded',
|
||||
outputStyle: 'nested'
|
||||
},
|
||||
import React from 'react'
|
||||
|
||||
files: {
|
||||
"dist/<%= grunt.config.get('pkg.name') %>.css": 'apps/common/css/main.scss',
|
||||
export default Stateless =>
|
||||
// eslint-disable-next-line react/prefer-stateless-function
|
||||
class extends React.Component {
|
||||
render() {
|
||||
return <Stateless {...this.props} />
|
||||
}
|
||||
}
|
||||
};
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (C) 2014 - present Instructure, Inc.
|
||||
* Copyright (C) 2021 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
|
@ -16,7 +16,7 @@
|
|||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
define({
|
||||
export default {
|
||||
PROGRESS_ATTRS: [
|
||||
'id',
|
||||
'completion',
|
||||
|
@ -24,15 +24,10 @@ define({
|
|||
'workflow_state'
|
||||
],
|
||||
|
||||
ATTACHMENT_ATTRS: [
|
||||
'created_at',
|
||||
'url'
|
||||
],
|
||||
ATTACHMENT_ATTRS: ['created_at', 'url'],
|
||||
|
||||
PROGRESS_QUEUED: 'queued',
|
||||
PROGRESS_ACTIVE: 'running',
|
||||
PROGRESS_COMPLETE: 'completed',
|
||||
PROGRESS_FAILED: 'failed',
|
||||
|
||||
KC_RETURN: 13,
|
||||
});
|
||||
PROGRESS_FAILED: 'failed'
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* Copyright (C) 2021 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas 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, version 3 of the License.
|
||||
*
|
||||
* Canvas 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/>.
|
||||
*/
|
||||
|
||||
import $ from 'jquery'
|
||||
|
||||
const Adapter = function(inputConfig) {
|
||||
this.config = inputConfig
|
||||
}
|
||||
|
||||
Adapter.prototype.request = function(options) {
|
||||
const ajax = this.config.ajax || $.ajax
|
||||
|
||||
options.headers = options.headers || {}
|
||||
options.headers['Content-Type'] = 'application/json'
|
||||
options.headers.Accept = 'application/vnd.api+json'
|
||||
|
||||
if (this.config.apiToken) {
|
||||
options.headers.Authorization = 'Bearer ' + this.config.apiToken
|
||||
}
|
||||
|
||||
if (options.type !== 'GET' && options.data) {
|
||||
options.data = JSON.stringify(options.data)
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
ajax(options).then(resolve, reject)
|
||||
})
|
||||
}
|
||||
|
||||
export default Adapter
|
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
* Copyright (C) 2021 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas 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, version 3 of the License.
|
||||
*
|
||||
* Canvas 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/>.
|
||||
*/
|
||||
|
||||
const callbacks = {}
|
||||
let gActionIndex = 0
|
||||
|
||||
const Dispatcher = function(inputConfig) {
|
||||
this.config = inputConfig
|
||||
}
|
||||
|
||||
Dispatcher.prototype.dispatch = function(action, params) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const actionIndex = ++gActionIndex
|
||||
const callback = callbacks[action]
|
||||
|
||||
if (callback) {
|
||||
callback(params, resolve, reject)
|
||||
} else {
|
||||
reject(new Error('Unknown action "' + action + '"'))
|
||||
}
|
||||
|
||||
return actionIndex
|
||||
})
|
||||
}
|
||||
|
||||
Dispatcher.prototype.register = function(action, callback) {
|
||||
if (callbacks[action]) {
|
||||
throw new Error("A handler is already registered to '" + action + "'")
|
||||
}
|
||||
|
||||
callbacks[action] = callback
|
||||
|
||||
return callback
|
||||
}
|
||||
|
||||
Dispatcher.prototype.clear = function() {
|
||||
for (const action of Object.keys(callbacks)) {
|
||||
callbacks[action] = null
|
||||
}
|
||||
}
|
||||
|
||||
export default Dispatcher
|
|
@ -0,0 +1,125 @@
|
|||
/*
|
||||
* Copyright (C) 2021 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas 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, version 3 of the License.
|
||||
*
|
||||
* Canvas 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/>.
|
||||
*/
|
||||
|
||||
import $ from 'jquery'
|
||||
import _ from 'lodash.underscore'
|
||||
|
||||
const extend = _.extend
|
||||
|
||||
/**
|
||||
* @class Common.Core.Environment
|
||||
*
|
||||
* API for manipulating the search query.
|
||||
*/
|
||||
const Environment = {
|
||||
/**
|
||||
* @property {Object} query
|
||||
* The extracted GET query parameters. See #parseQueryString
|
||||
*/
|
||||
query: {},
|
||||
|
||||
/**
|
||||
* Extract query parameters from a query string. The method can handle
|
||||
* scalar and 1-level array values.
|
||||
*
|
||||
* @param {String} query
|
||||
* The query string from the location bar. Like:
|
||||
* "?foo=bar" or "foo=bar&arr[]=1&arr[]=2"
|
||||
*
|
||||
* @return {Object}
|
||||
* Contains the key-value pairs found in the query string.
|
||||
*/
|
||||
parseQueryString(query) {
|
||||
const items = query.replace(/^\?/, '').split('&')
|
||||
|
||||
return items.reduce(function(params, item) {
|
||||
const pair = item.split('=')
|
||||
let key = decodeURIComponent(pair[0])
|
||||
const value = decodeURIComponent(pair[1])
|
||||
|
||||
if (key && key.length) {
|
||||
if (key.substr(-2, 2) === '[]') {
|
||||
key = key.substr(0, key.length - 2)
|
||||
|
||||
params[key] = params[key] || []
|
||||
params[key].push(value)
|
||||
} else {
|
||||
params[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
return params
|
||||
}, {})
|
||||
},
|
||||
|
||||
/**
|
||||
* Create or replace a bunch of parameters in the query string.
|
||||
*
|
||||
* @example
|
||||
* // Say the search has something like ?foo=bar&from=03/01/2014
|
||||
* Env.updateQueryString({
|
||||
* from: "03/28/2014"
|
||||
* });
|
||||
* // => ?foo=bar&from=03/28/2014
|
||||
*
|
||||
*/
|
||||
updateQueryString(params) {
|
||||
this.query = extend({}, this.query, params)
|
||||
|
||||
window.history.pushState(
|
||||
'',
|
||||
'',
|
||||
[window.location.pathname, decodeURIComponent($.param(this.query))].join('?')
|
||||
)
|
||||
},
|
||||
|
||||
getQueryParameter(key) {
|
||||
return this.query[key]
|
||||
},
|
||||
|
||||
removeQueryParameter(key) {
|
||||
this.removeQueryParameters([key])
|
||||
},
|
||||
|
||||
removeQueryParameters(keys) {
|
||||
const query = this.query
|
||||
|
||||
keys.forEach(function(key) {
|
||||
delete query[key]
|
||||
})
|
||||
|
||||
this.updateQueryString({})
|
||||
}
|
||||
}
|
||||
|
||||
// Extract the actual query string either from location.search if it's there,
|
||||
// or from the hash if we're using hash-based history, or from the href
|
||||
// as the last resort.
|
||||
const extractQueryString = function() {
|
||||
if (window.location.search.length) {
|
||||
return window.location.search
|
||||
} else if (window.location.hash.length) {
|
||||
return window.location.hash.split('?')[1] || ''
|
||||
} else {
|
||||
return window.location.href.split('?')[1] || ''
|
||||
}
|
||||
}
|
||||
|
||||
Environment.query = Environment.parseQueryString(extractQueryString())
|
||||
|
||||
export default Environment
|
|
@ -0,0 +1,99 @@
|
|||
/*
|
||||
* Copyright (C) 2021 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas 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, version 3 of the License.
|
||||
*
|
||||
* Canvas 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/>.
|
||||
*/
|
||||
|
||||
import _ from 'lodash.underscore'
|
||||
|
||||
const extend = _.extend
|
||||
|
||||
const Store = function(key, proto, Dispatcher) {
|
||||
const emitChange = this.emitChange.bind(this)
|
||||
|
||||
extend(this, proto || {})
|
||||
|
||||
this._key = key
|
||||
this.__reset__()
|
||||
|
||||
Object.keys(this.actions).forEach(
|
||||
function(action) {
|
||||
const handler = this.actions[action].bind(this)
|
||||
const scopedAction = [key, action].join(':')
|
||||
|
||||
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(callback) {
|
||||
this._callbacks.push(callback)
|
||||
},
|
||||
|
||||
removeChangeListener(callback) {
|
||||
const index = this._callbacks.indexOf(callback)
|
||||
if (index > -1) {
|
||||
this._callbacks.splice(index, 1)
|
||||
}
|
||||
},
|
||||
|
||||
emitChange() {
|
||||
this._callbacks.forEach(function(callback) {
|
||||
callback()
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* @private
|
||||
*
|
||||
* 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__() {
|
||||
this._callbacks = []
|
||||
this.state = this.getInitialState()
|
||||
},
|
||||
|
||||
getInitialState() {
|
||||
return {}
|
||||
},
|
||||
|
||||
setState(newState) {
|
||||
extend(this.state, newState)
|
||||
this.emitChange()
|
||||
}
|
||||
})
|
||||
|
||||
export default Store
|
|
@ -1,6 +1,5 @@
|
|||
/** @jsx React.DOM */
|
||||
/*
|
||||
* Copyright (C) 2014 - present Instructure, Inc.
|
||||
* Copyright (C) 2021 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
|
@ -17,16 +16,20 @@
|
|||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
define(function(require) {
|
||||
var React = require('../../ext/react');
|
||||
var Essay = require('jsx!./essay');
|
||||
export default function fromJSONAPI(payload, collKey, wantsObject) {
|
||||
let data = {}
|
||||
|
||||
var Calculated = React.createClass({
|
||||
render: Essay.type.prototype.render,
|
||||
renderLinkButton: function() {
|
||||
return false;
|
||||
if (payload) {
|
||||
if (payload[collKey]) {
|
||||
data = payload[collKey]
|
||||
} else {
|
||||
data = payload
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return Calculated;
|
||||
});
|
||||
if (wantsObject && Array.isArray(data)) {
|
||||
return data[0]
|
||||
} else {
|
||||
return data
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue