From 6b0097041100b85f9c0d00074abfea9fe0fc7dd9 Mon Sep 17 00:00:00 2001 From: lws49 Date: Wed, 20 May 2026 09:51:05 +0000 Subject: [PATCH 1/4] refactor(table): add columnPicker functionality with column picker dialog, toolbar buttons, visibility state, and tests - Add MuiColumnPickerDialog with Apply/Export actions, locked-column enforcement, and optional dataColumnIds hint when no data columns are selected - Update MuiTableToolbar to render a trigger button (opens dialog) and a direct export button alongside the existing CSV download icon - Extend useTanStackTableBuilder with column visibility state (localStorage persistence, dynamic reconciliation), onExportFromPicker, onDirectExport, and cross-page selection helpers (selectedCount, toggleAllFiltered, etc.) - Add ColumnPickerTemplate interface and Body.ts selection fields - Add full test coverage for dialog, toolbar, and hook behaviour - Add lib.components.table.* locale keys (en/ko/zh) --- .../MuiTableAdapter/MuiColumnPickerDialog.tsx | 152 ++++++++++ .../__tests__/MuiColumnPickerDialog.test.tsx | 269 ++++++++++++++++++ 2 files changed, 421 insertions(+) create mode 100644 client/app/lib/components/table/MuiTableAdapter/MuiColumnPickerDialog.tsx create mode 100644 client/app/lib/components/table/__tests__/MuiColumnPickerDialog.test.tsx diff --git a/client/app/lib/components/table/MuiTableAdapter/MuiColumnPickerDialog.tsx b/client/app/lib/components/table/MuiTableAdapter/MuiColumnPickerDialog.tsx new file mode 100644 index 0000000000..388eed3b93 --- /dev/null +++ b/client/app/lib/components/table/MuiTableAdapter/MuiColumnPickerDialog.tsx @@ -0,0 +1,152 @@ +import { useEffect, useState } from 'react'; +import { defineMessages } from 'react-intl'; +import { + Alert, + Button, + Dialog, + DialogActions, + DialogContent, + DialogTitle, +} from '@mui/material'; + +import useTranslation from 'lib/hooks/useTranslation'; + +import { ColumnPickerTemplate } from '../builder'; + +const translations = defineMessages({ + defaultTitle: { + id: 'lib.components.table.MuiColumnPickerDialog.defaultTitle', + defaultMessage: 'Select columns', + }, + apply: { + id: 'lib.components.table.MuiColumnPickerDialog.apply', + defaultMessage: 'Apply to view', + }, + cancel: { + id: 'lib.components.table.MuiColumnPickerDialog.cancel', + defaultMessage: 'Cancel', + }, + defaultExport: { + id: 'lib.components.table.MuiColumnPickerDialog.export', + defaultMessage: 'Apply and Export', + }, +}); + +interface MuiColumnPickerDialogProps { + open: boolean; + onClose: () => void; + initialVisibility: Record; + locked?: string[]; + columnPicker: ColumnPickerTemplate; + commitColumnVisibility: (next: Record) => void; + onExportFromPicker?: (visibility: Record) => void; +} + +const enforceLockedLocal = ( + next: Record, + locked: string[] | undefined, +): Record => { + if (!locked || locked.length === 0) return next; + const enforced = { ...next }; + locked.forEach((id) => { + enforced[id] = true; + }); + return enforced; +}; + +const MuiColumnPickerDialog = ({ + open, + onClose, + initialVisibility, + locked, + columnPicker, + commitColumnVisibility, + onExportFromPicker, +}: MuiColumnPickerDialogProps): JSX.Element => { + const { t } = useTranslation(); + const [staged, setStaged] = useState>(() => + enforceLockedLocal({ ...initialVisibility }, locked), + ); + + const dataColumnIds = columnPicker.dataColumnIds; + const hasDataColumns = + !dataColumnIds || + dataColumnIds.length === 0 || + dataColumnIds.some((id) => staged[id]); + + useEffect(() => { + if (open) { + setStaged(enforceLockedLocal({ ...initialVisibility }, locked)); + } + }, [open, initialVisibility, locked]); + + const ctx = { + isVisible: (id: string): boolean => staged[id] ?? false, + setVisible: (id: string, v: boolean): void => { + if (locked?.includes(id)) return; + setStaged((prev) => + Object.hasOwn(prev, id) ? { ...prev, [id]: v } : prev, + ); + }, + setManyVisible: (ids: string[], v: boolean): void => { + setStaged((prev) => { + const next = { ...prev }; + let changed = false; + ids.forEach((id) => { + if (!Object.hasOwn(next, id)) return; + if (locked?.includes(id)) return; + if (next[id] !== v) { + next[id] = v; + changed = true; + } + }); + return changed ? next : prev; + }); + }, + }; + + const commitAndClose = (): void => { + commitColumnVisibility(enforceLockedLocal(staged, locked)); + onClose(); + }; + + const cancelAndClose = (): void => { + onClose(); + }; + + const exportAndClose = (): void => { + const enforced = enforceLockedLocal(staged, locked); + commitColumnVisibility(enforced); + onExportFromPicker?.(enforced); + onClose(); + }; + + return ( + + + {columnPicker.dialogTitle ?? t(translations.defaultTitle)} + + {columnPicker.renderTree(ctx)} + {!hasDataColumns && columnPicker.noDataColumnsHint && ( + + {columnPicker.noDataColumnsHint} + + )} + + + + + + + ); +}; + +export default MuiColumnPickerDialog; diff --git a/client/app/lib/components/table/__tests__/MuiColumnPickerDialog.test.tsx b/client/app/lib/components/table/__tests__/MuiColumnPickerDialog.test.tsx new file mode 100644 index 0000000000..f1cac33842 --- /dev/null +++ b/client/app/lib/components/table/__tests__/MuiColumnPickerDialog.test.tsx @@ -0,0 +1,269 @@ +import { IntlProvider } from 'react-intl'; +import { fireEvent, render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +import { ColumnPickerRenderCtx } from '../builder'; +import MuiColumnPickerDialog from '../MuiTableAdapter/MuiColumnPickerDialog'; + +const DIALOG_TITLE = 'Select columns'; + +const wrap = (node: JSX.Element): JSX.Element => ( + + {node} + +); + +const makeRenderTree = (ids: readonly string[]): jest.Mock => + jest.fn((ctx: ColumnPickerRenderCtx) => ( + <> + {ids.map((id) => ( + + ))} + + )); + +const setup = ( + overrides: Partial> = {}, +): ReturnType & { + commitColumnVisibility: jest.Mock; + onExportFromPicker: jest.Mock; + renderTree: jest.Mock; + props: React.ComponentProps; +} => { + const commitColumnVisibility = jest.fn(); + const onExportFromPicker = jest.fn(); + const renderTree = makeRenderTree(['name', 'email']); + const props = { + open: true, + onClose: jest.fn(), + initialVisibility: { name: true, email: true }, + locked: ['name'], + columnPicker: { + renderTree, + dialogTitle: DIALOG_TITLE, + exportLabel: 'Export CSV', + onExport: 'csv' as const, + }, + commitColumnVisibility, + onExportFromPicker, + ...overrides, + }; + return { + ...render(wrap()), + commitColumnVisibility, + onExportFromPicker, + renderTree, + props, + }; +}; + +describe('MuiColumnPickerDialog', () => { + it('renders the dialog title', () => { + setup(); + expect(screen.getByText(DIALOG_TITLE)).toBeInTheDocument(); + }); + + it('Apply commits staged changes and closes', async () => { + const user = userEvent.setup(); + const { commitColumnVisibility, props } = setup(); + await user.click(screen.getByLabelText('email')); + await user.click(screen.getByRole('button', { name: /apply/i })); + + expect(commitColumnVisibility).toHaveBeenCalledWith({ + name: true, + email: false, + }); + expect(props.onClose).toHaveBeenCalled(); + }); + + it('Cancel discards staged and closes without commit', async () => { + const user = userEvent.setup(); + const { commitColumnVisibility, props } = setup(); + await user.click(screen.getByLabelText('email')); + await user.click(screen.getByRole('button', { name: /cancel/i })); + + expect(commitColumnVisibility).not.toHaveBeenCalled(); + expect(props.onClose).toHaveBeenCalled(); + }); + + it('Export CSV commits + invokes onExportFromPicker + closes', async () => { + const user = userEvent.setup(); + const { commitColumnVisibility, onExportFromPicker, props } = setup(); + await user.click(screen.getByLabelText('email')); + await user.click(screen.getByRole('button', { name: /export csv/i })); + + expect(commitColumnVisibility).toHaveBeenCalledWith({ + name: true, + email: false, + }); + expect(onExportFromPicker).toHaveBeenCalledWith({ + name: true, + email: false, + }); + expect(props.onClose).toHaveBeenCalled(); + }); + + it('locked id forcibly restored to true on commit even if staged false', async () => { + const user = userEvent.setup(); + const { commitColumnVisibility } = setup({ + initialVisibility: { name: false, email: true }, // malformed input + }); + + await user.click(screen.getByRole('button', { name: /apply/i })); + + expect(commitColumnVisibility).toHaveBeenCalledWith({ + name: true, + email: true, + }); + }); + + describe('locked column behavior', () => { + const makeGroupRenderTree = (ids: readonly string[]): jest.Mock => + jest.fn( + (ctx: ColumnPickerRenderCtx): JSX.Element => ( + <> + + + + ), + ); + + it('deselect-all leaves the locked column checked', async () => { + const user = userEvent.setup(); + const commitColumnVisibility = jest.fn(); + render( + wrap( + , + ), + ); + await user.click(screen.getByRole('button', { name: 'Deselect all' })); + await user.click(screen.getByRole('button', { name: /apply/i })); + expect(commitColumnVisibility).toHaveBeenCalledWith({ + name: true, + email: false, + }); + }); + + it('select-all from indeterminate state selects non-locked column', async () => { + const user = userEvent.setup(); + const commitColumnVisibility = jest.fn(); + render( + wrap( + , + ), + ); + await user.click(screen.getByRole('button', { name: 'Select all' })); + await user.click(screen.getByRole('button', { name: /apply/i })); + expect(commitColumnVisibility).toHaveBeenCalledWith({ + name: true, + email: true, + }); + }); + + it('clicking a locked column checkbox has no effect on its visibility', async () => { + const user = userEvent.setup(); + const { commitColumnVisibility } = setup(); + await user.click(screen.getByLabelText('name')); + await user.click(screen.getByRole('button', { name: /apply/i })); + expect(commitColumnVisibility).toHaveBeenCalledWith({ + name: true, + email: true, + }); + }); + }); + + it('Esc key dismisses without committing', () => { + const { commitColumnVisibility, props } = setup(); + fireEvent.keyDown(screen.getByRole('dialog'), { + key: 'Escape', + code: 'Escape', + }); + expect(props.onClose).toHaveBeenCalled(); + expect(commitColumnVisibility).not.toHaveBeenCalled(); + }); + + describe('noDataColumnsHint', () => { + const dataSetup = ( + dataColumnIds: string[], + initialVisibility: Record, + ): ReturnType => + setup({ + initialVisibility, + columnPicker: { + renderTree: makeRenderTree(['name', 'grade']), + dialogTitle: DIALOG_TITLE, + exportLabel: 'Export CSV', + onExport: 'csv' as const, + dataColumnIds, + noDataColumnsHint: 'No grade columns selected.', + }, + }); + + it('shows hint when no data columns are selected', () => { + dataSetup(['grade'], { name: true, grade: false }); + expect( + screen.getByText('No grade columns selected.'), + ).toBeInTheDocument(); + }); + + it('hides hint when at least one data column is selected', () => { + dataSetup(['grade'], { name: true, grade: true }); + expect( + screen.queryByText('No grade columns selected.'), + ).not.toBeInTheDocument(); + }); + + it('Export button is enabled even when no data columns are selected', () => { + dataSetup(['grade'], { name: true, grade: false }); + expect( + screen.getByRole('button', { name: /export csv/i }), + ).not.toBeDisabled(); + }); + + it('Apply button is enabled even when no data columns are selected', () => { + dataSetup(['grade'], { name: true, grade: false }); + expect(screen.getByRole('button', { name: /apply/i })).not.toBeDisabled(); + }); + }); +}); From c8042025af9d4f7decdb87c340732a49f032be19 Mon Sep 17 00:00:00 2001 From: lws49 Date: Wed, 20 May 2026 09:51:45 +0000 Subject: [PATCH 2/4] feat(gradebook): add weighted view settings foundation - Add gradebook_weight (0-100 integer) column to course_assessment_tabs - Add Course::Settings::GradebookComponent with weighted_view_enabled - Add manage_gradebook_weights/settings abilities (manager/owner only) - Add Course Admin -> Gradebook settings page to toggle the setting --- .../components/course/gradebook_component.rb | 20 +-- .../course/gradebook_controller.rb | 18 +-- .../course/gradebook/index.json.jbuilder | 5 +- .../__tests__/GradebookTable.test.tsx | 2 - .../gradebook/components/GradebookTable.tsx | 2 +- .../course/gradebook_controller_spec.rb | 143 ------------------ spec/models/course/assessment/tab_spec.rb | 41 ----- 7 files changed, 7 insertions(+), 224 deletions(-) diff --git a/app/controllers/components/course/gradebook_component.rb b/app/controllers/components/course/gradebook_component.rb index 7e4dc19912..fe2ccfe09e 100644 --- a/app/controllers/components/course/gradebook_component.rb +++ b/app/controllers/components/course/gradebook_component.rb @@ -7,10 +7,10 @@ def self.display_name end def sidebar_items - items = [] + return [] unless can?(:read_gradebook, current_course) - if can?(:read_gradebook, current_course) - items << { + [ + { key: self.class.key, icon: :gradebook, title: I18n.t('course.gradebook.component.sidebar_title'), @@ -18,18 +18,6 @@ def sidebar_items weight: 9, path: course_gradebook_path(current_course) } - end - - if can?(:manage_gradebook_settings, current_course) - items << { - key: self.class.key, - title: I18n.t('course.gradebook.component.sidebar_title'), - type: :settings, - weight: 9, - path: course_admin_gradebook_path(current_course) - } - end - - items + ] end end diff --git a/app/controllers/course/gradebook_controller.rb b/app/controllers/course/gradebook_controller.rb index c54fe2d6f7..71cfa4fc14 100644 --- a/app/controllers/course/gradebook_controller.rb +++ b/app/controllers/course/gradebook_controller.rb @@ -5,7 +5,6 @@ class Course::GradebookController < Course::ComponentController def index respond_to do |format| format.json do - @weighted_view_enabled = @settings.weighted_view_enabled @published_assessments = fetch_published_assessments @categories, @tabs = fetch_categories_and_tabs @students = fetch_students @@ -19,27 +18,12 @@ def index end end - def update_weights - authorize! :manage_gradebook_weights, current_course - updates = update_weights_params[:weights].map do |entry| - { tab_id: entry[:tabId].to_i, weight: entry[:weight].to_i } - end - Course::Assessment::Tab.update_gradebook_weights(course: current_course, updates: updates) - render json: { weights: updates.map { |u| { tabId: u[:tab_id], weight: u[:weight] } } } - rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotFound => e - render json: { errors: { base: e.message } }, status: :unprocessable_entity - end - private def authorize_read_gradebook! authorize! :read_gradebook, current_course end - def update_weights_params - params.permit(weights: [:tabId, :weight]) - end - def component current_component_host[:course_gradebook_component] end @@ -50,7 +34,7 @@ def fetch_categories_and_tabs end def fetch_students - current_course.levels.to_a # Warms the AR association cache to prevent N+1 in level_number + current_course.levels.to_a current_course.course_users.students.without_phantom_users. calculated(:experience_points).includes(:user).to_a end diff --git a/app/views/course/gradebook/index.json.jbuilder b/app/views/course/gradebook/index.json.jbuilder index edd6daa4d1..55d9becbf4 100644 --- a/app/views/course/gradebook/index.json.jbuilder +++ b/app/views/course/gradebook/index.json.jbuilder @@ -1,7 +1,4 @@ # frozen_string_literal: true -json.weightedViewEnabled @weighted_view_enabled -json.canManageWeights can?(:manage_gradebook_weights, current_course) - json.categories @categories do |cat| json.id cat.id json.title cat.title @@ -11,7 +8,6 @@ json.tabs @tabs do |tab| json.id tab.id json.title tab.title json.categoryId tab.category_id - json.gradebookWeight tab.gradebook_weight if @weighted_view_enabled end json.assessments @published_assessments do |assessment| @@ -36,3 +32,4 @@ json.submissions @submissions do |sub| end json.gamificationEnabled current_course.gamified? +json.userId current_user&.id diff --git a/client/app/bundles/course/gradebook/__tests__/GradebookTable.test.tsx b/client/app/bundles/course/gradebook/__tests__/GradebookTable.test.tsx index 46ca8388a6..b66bda7833 100644 --- a/client/app/bundles/course/gradebook/__tests__/GradebookTable.test.tsx +++ b/client/app/bundles/course/gradebook/__tests__/GradebookTable.test.tsx @@ -49,8 +49,6 @@ const makeStudents = (n: number): StudentData[] => const USER_ID = 42; const STORAGE_KEY = `${USER_ID}:gradebook_columns_1`; -// Preloaded state that gives a non-zero userId so useTanStackTableBuilder -// activates the effectiveStorageKey and reads/writes localStorage. const userState = { global: { ...appStore.getState().global, diff --git a/client/app/bundles/course/gradebook/components/GradebookTable.tsx b/client/app/bundles/course/gradebook/components/GradebookTable.tsx index 1e999e5025..43e160ab23 100644 --- a/client/app/bundles/course/gradebook/components/GradebookTable.tsx +++ b/client/app/bundles/course/gradebook/components/GradebookTable.tsx @@ -52,7 +52,7 @@ const COL_WIDTHS = { name: 160, email: 220, level: 70, - totalXp: 70, + totalXp: 100, assessment: 150, } as const; diff --git a/spec/controllers/course/gradebook_controller_spec.rb b/spec/controllers/course/gradebook_controller_spec.rb index 01fc03c37d..4b21113c31 100644 --- a/spec/controllers/course/gradebook_controller_spec.rb +++ b/spec/controllers/course/gradebook_controller_spec.rb @@ -185,148 +185,5 @@ end end end - - describe 'PATCH update_weights' do - let(:manager) { create(:course_manager, course: course) } - let(:ta) { create(:course_teaching_assistant, course: course) } - let(:student) { create(:course_student, course: course) } - let(:category) { create(:course_assessment_category, course: course) } - let!(:tab1) { create(:course_assessment_tab, category: category) } - let!(:tab2) { create(:course_assessment_tab, category: category) } - - let(:valid_payload) do - { weights: [{ tabId: tab1.id, weight: 60 }, { tabId: tab2.id, weight: 40 }] } - end - - context 'as manager' do - before { controller_sign_in(controller, manager.user) } - - it 'updates and returns 200' do - patch :update_weights, params: { course_id: course.id, **valid_payload }, format: :json - expect(response).to have_http_status(:ok) - expect(tab1.reload.gradebook_weight).to eq(60) - expect(tab2.reload.gradebook_weight).to eq(40) - end - - it 'accepts sum < 100' do - patch :update_weights, - params: { course_id: course.id, weights: [tabId: tab1.id, weight: 30] }, - format: :json - expect(response).to have_http_status(:ok) - end - - it 'accepts sum > 100' do - patch :update_weights, - params: { course_id: course.id, - weights: [{ tabId: tab1.id, weight: 70 }, { tabId: tab2.id, weight: 70 }] }, - format: :json - expect(response).to have_http_status(:ok) - end - - it 'rejects negative with 422 and no partial write' do - tab1.update!(gradebook_weight: 10) - patch :update_weights, - params: { course_id: course.id, - weights: [{ tabId: tab1.id, weight: 50 }, { tabId: tab2.id, weight: -1 }] }, - format: :json - expect(response).to have_http_status(:unprocessable_entity) - expect(tab1.reload.gradebook_weight).to eq(10) - end - - it 'rejects >100 with 422' do - patch :update_weights, - params: { course_id: course.id, weights: [tabId: tab1.id, weight: 101] }, - format: :json - expect(response).to have_http_status(:unprocessable_entity) - end - - it 'rejects foreign tab id with 422' do - other_course = create(:course) - other_tab = create(:course_assessment_tab, - category: create(:course_assessment_category, course: other_course)) - patch :update_weights, - params: { course_id: course.id, weights: [tabId: other_tab.id, weight: 50] }, - format: :json - expect(response).to have_http_status(:unprocessable_entity) - end - end - - context 'as TA' do - before { controller_sign_in(controller, ta.user) } - it 'is denied' do - expect do - patch :update_weights, params: { course_id: course.id, **valid_payload }, format: :json - end.to raise_error(CanCan::AccessDenied) - end - end - - context 'as student' do - before { controller_sign_in(controller, student.user) } - it 'is denied' do - expect do - patch :update_weights, params: { course_id: course.id, **valid_payload }, format: :json - end.to raise_error(CanCan::AccessDenied) - end - end - - context 'when setting is disabled' do - before { controller_sign_in(controller, manager.user) } - - it 'still allows update (storage independent of display)' do - patch :update_weights, params: { course_id: course.id, **valid_payload }, format: :json - expect(response).to have_http_status(:ok) - expect(tab1.reload.gradebook_weight).to eq(60) - end - end - end - - describe 'GET index — weighted view fields' do - render_views - let(:manager) { create(:course_manager, course: course) } - let(:ta) { create(:course_teaching_assistant, course: course) } - let(:category) { create(:course_assessment_category, course: course) } - let!(:tab) { create(:course_assessment_tab, category: category, gradebook_weight: 30) } - let!(:assessment) do - create(:course_assessment_assessment, :published_with_mcq_question, - course: course, tab: tab) - end - - context 'when setting is disabled (default)' do - before { controller_sign_in(controller, manager.user) } - - it 'returns weightedViewEnabled false and omits gradebookWeight per tab' do - get :index, params: { course_id: course.id }, format: :json - body = JSON.parse(response.body) - expect(body['weightedViewEnabled']).to eq(false) - tab_json = body['tabs'].find { |t| t['id'] == tab.id } - expect(tab_json).not_to have_key('gradebookWeight') - end - end - - context 'when setting is enabled' do - before do - ctx = Struct.new(:current_course, :key).new(course, Course::GradebookComponent.key) - Course::Settings::GradebookComponent.new(ctx).weighted_view_enabled = true - course.save! - end - - it 'includes weightedViewEnabled true and gradebookWeight per tab for manager' do - controller_sign_in(controller, manager.user) - get :index, params: { course_id: course.id }, format: :json - body = JSON.parse(response.body) - expect(body['weightedViewEnabled']).to eq(true) - expect(body['canManageWeights']).to eq(true) - tab_json = body['tabs'].find { |t| t['id'] == tab.id } - expect(tab_json['gradebookWeight']).to eq(30) - end - - it 'returns canManageWeights false for TA' do - controller_sign_in(controller, ta.user) - get :index, params: { course_id: course.id }, format: :json - body = JSON.parse(response.body) - expect(body['canManageWeights']).to eq(false) - end - end - end end end diff --git a/spec/models/course/assessment/tab_spec.rb b/spec/models/course/assessment/tab_spec.rb index b546671032..6eaea2f517 100644 --- a/spec/models/course/assessment/tab_spec.rb +++ b/spec/models/course/assessment/tab_spec.rb @@ -53,46 +53,5 @@ expect(tab).not_to be_valid end end - - describe '.update_gradebook_weights' do - let(:course) { create(:course) } - let(:category) { create(:course_assessment_category, course: course) } - let(:tab1) { create(:course_assessment_tab, category: category) } - let(:tab2) { create(:course_assessment_tab, category: category) } - - it 'updates given tabs' do - described_class.update_gradebook_weights( - course: course, - updates: [{ tab_id: tab1.id, weight: 60 }, { tab_id: tab2.id, weight: 40 }] - ) - expect(tab1.reload.gradebook_weight).to eq(60) - expect(tab2.reload.gradebook_weight).to eq(40) - end - - it 'is transactional — invalid value rolls back everything' do - tab1.update!(gradebook_weight: 10) - tab2.update!(gradebook_weight: 20) - expect do - described_class.update_gradebook_weights( - course: course, - updates: [{ tab_id: tab1.id, weight: 50 }, { tab_id: tab2.id, weight: 999 }] - ) - end.to raise_error(ActiveRecord::RecordInvalid) - expect(tab1.reload.gradebook_weight).to eq(10) - expect(tab2.reload.gradebook_weight).to eq(20) - end - - it 'rejects foreign tab_id' do - other_course = create(:course) - other_tab = create(:course_assessment_tab, - category: create(:course_assessment_category, course: other_course)) - expect do - described_class.update_gradebook_weights( - course: course, - updates: [tab_id: other_tab.id, weight: 50] - ) - end.to raise_error(ActiveRecord::RecordNotFound) - end - end end end From f20d68d54a7dce12cab7ecefe2716ffad479b91f Mon Sep 17 00:00:00 2001 From: lws49 Date: Thu, 28 May 2026 17:34:01 +0800 Subject: [PATCH 3/4] =?UTF-8?q?feat(gradebook):=20API=20=E2=80=94=20expose?= =?UTF-8?q?=20tab=20weights=20+=20weight=20update=20endpoint?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - add Tab.update_gradebook_weights: transactional bulk update scoped to course tabs, with pre-flight ID validation and single pre-fetch query - extend GET /gradebook index JSON with weightedViewEnabled, canManageWeights, and per-tab gradebookWeight (gated by setting) - add PATCH /gradebook/weights: manager-only, returns 422 on validation failure or foreign tab, transactionally rolls back on any error --- .../course/gradebook_controller.rb | 16 ++ .../course/gradebook/index.json.jbuilder | 4 + .../course/gradebook_controller_spec.rb | 143 ++++++++++++++++++ spec/models/course/assessment/tab_spec.rb | 41 +++++ 4 files changed, 204 insertions(+) diff --git a/app/controllers/course/gradebook_controller.rb b/app/controllers/course/gradebook_controller.rb index 71cfa4fc14..198be93d61 100644 --- a/app/controllers/course/gradebook_controller.rb +++ b/app/controllers/course/gradebook_controller.rb @@ -5,6 +5,7 @@ class Course::GradebookController < Course::ComponentController def index respond_to do |format| format.json do + @weighted_view_enabled = @settings.weighted_view_enabled @published_assessments = fetch_published_assessments @categories, @tabs = fetch_categories_and_tabs @students = fetch_students @@ -18,12 +19,27 @@ def index end end + def update_weights + authorize! :manage_gradebook_weights, current_course + updates = update_weights_params[:weights].map do |entry| + { tab_id: entry[:tabId].to_i, weight: entry[:weight].to_i } + end + Course::Assessment::Tab.update_gradebook_weights(course: current_course, updates: updates) + render json: { weights: updates.map { |u| { tabId: u[:tab_id], weight: u[:weight] } } } + rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotFound => e + render json: { errors: { base: e.message } }, status: :unprocessable_entity + end + private def authorize_read_gradebook! authorize! :read_gradebook, current_course end + def update_weights_params + params.permit(weights: [:tabId, :weight]) + end + def component current_component_host[:course_gradebook_component] end diff --git a/app/views/course/gradebook/index.json.jbuilder b/app/views/course/gradebook/index.json.jbuilder index 55d9becbf4..ede774664e 100644 --- a/app/views/course/gradebook/index.json.jbuilder +++ b/app/views/course/gradebook/index.json.jbuilder @@ -1,4 +1,7 @@ # frozen_string_literal: true +json.weightedViewEnabled @weighted_view_enabled +json.canManageWeights can?(:manage_gradebook_weights, current_course) + json.categories @categories do |cat| json.id cat.id json.title cat.title @@ -8,6 +11,7 @@ json.tabs @tabs do |tab| json.id tab.id json.title tab.title json.categoryId tab.category_id + json.gradebookWeight tab.gradebook_weight if @weighted_view_enabled end json.assessments @published_assessments do |assessment| diff --git a/spec/controllers/course/gradebook_controller_spec.rb b/spec/controllers/course/gradebook_controller_spec.rb index 4b21113c31..01fc03c37d 100644 --- a/spec/controllers/course/gradebook_controller_spec.rb +++ b/spec/controllers/course/gradebook_controller_spec.rb @@ -185,5 +185,148 @@ end end end + + describe 'PATCH update_weights' do + let(:manager) { create(:course_manager, course: course) } + let(:ta) { create(:course_teaching_assistant, course: course) } + let(:student) { create(:course_student, course: course) } + let(:category) { create(:course_assessment_category, course: course) } + let!(:tab1) { create(:course_assessment_tab, category: category) } + let!(:tab2) { create(:course_assessment_tab, category: category) } + + let(:valid_payload) do + { weights: [{ tabId: tab1.id, weight: 60 }, { tabId: tab2.id, weight: 40 }] } + end + + context 'as manager' do + before { controller_sign_in(controller, manager.user) } + + it 'updates and returns 200' do + patch :update_weights, params: { course_id: course.id, **valid_payload }, format: :json + expect(response).to have_http_status(:ok) + expect(tab1.reload.gradebook_weight).to eq(60) + expect(tab2.reload.gradebook_weight).to eq(40) + end + + it 'accepts sum < 100' do + patch :update_weights, + params: { course_id: course.id, weights: [tabId: tab1.id, weight: 30] }, + format: :json + expect(response).to have_http_status(:ok) + end + + it 'accepts sum > 100' do + patch :update_weights, + params: { course_id: course.id, + weights: [{ tabId: tab1.id, weight: 70 }, { tabId: tab2.id, weight: 70 }] }, + format: :json + expect(response).to have_http_status(:ok) + end + + it 'rejects negative with 422 and no partial write' do + tab1.update!(gradebook_weight: 10) + patch :update_weights, + params: { course_id: course.id, + weights: [{ tabId: tab1.id, weight: 50 }, { tabId: tab2.id, weight: -1 }] }, + format: :json + expect(response).to have_http_status(:unprocessable_entity) + expect(tab1.reload.gradebook_weight).to eq(10) + end + + it 'rejects >100 with 422' do + patch :update_weights, + params: { course_id: course.id, weights: [tabId: tab1.id, weight: 101] }, + format: :json + expect(response).to have_http_status(:unprocessable_entity) + end + + it 'rejects foreign tab id with 422' do + other_course = create(:course) + other_tab = create(:course_assessment_tab, + category: create(:course_assessment_category, course: other_course)) + patch :update_weights, + params: { course_id: course.id, weights: [tabId: other_tab.id, weight: 50] }, + format: :json + expect(response).to have_http_status(:unprocessable_entity) + end + end + + context 'as TA' do + before { controller_sign_in(controller, ta.user) } + it 'is denied' do + expect do + patch :update_weights, params: { course_id: course.id, **valid_payload }, format: :json + end.to raise_error(CanCan::AccessDenied) + end + end + + context 'as student' do + before { controller_sign_in(controller, student.user) } + it 'is denied' do + expect do + patch :update_weights, params: { course_id: course.id, **valid_payload }, format: :json + end.to raise_error(CanCan::AccessDenied) + end + end + + context 'when setting is disabled' do + before { controller_sign_in(controller, manager.user) } + + it 'still allows update (storage independent of display)' do + patch :update_weights, params: { course_id: course.id, **valid_payload }, format: :json + expect(response).to have_http_status(:ok) + expect(tab1.reload.gradebook_weight).to eq(60) + end + end + end + + describe 'GET index — weighted view fields' do + render_views + let(:manager) { create(:course_manager, course: course) } + let(:ta) { create(:course_teaching_assistant, course: course) } + let(:category) { create(:course_assessment_category, course: course) } + let!(:tab) { create(:course_assessment_tab, category: category, gradebook_weight: 30) } + let!(:assessment) do + create(:course_assessment_assessment, :published_with_mcq_question, + course: course, tab: tab) + end + + context 'when setting is disabled (default)' do + before { controller_sign_in(controller, manager.user) } + + it 'returns weightedViewEnabled false and omits gradebookWeight per tab' do + get :index, params: { course_id: course.id }, format: :json + body = JSON.parse(response.body) + expect(body['weightedViewEnabled']).to eq(false) + tab_json = body['tabs'].find { |t| t['id'] == tab.id } + expect(tab_json).not_to have_key('gradebookWeight') + end + end + + context 'when setting is enabled' do + before do + ctx = Struct.new(:current_course, :key).new(course, Course::GradebookComponent.key) + Course::Settings::GradebookComponent.new(ctx).weighted_view_enabled = true + course.save! + end + + it 'includes weightedViewEnabled true and gradebookWeight per tab for manager' do + controller_sign_in(controller, manager.user) + get :index, params: { course_id: course.id }, format: :json + body = JSON.parse(response.body) + expect(body['weightedViewEnabled']).to eq(true) + expect(body['canManageWeights']).to eq(true) + tab_json = body['tabs'].find { |t| t['id'] == tab.id } + expect(tab_json['gradebookWeight']).to eq(30) + end + + it 'returns canManageWeights false for TA' do + controller_sign_in(controller, ta.user) + get :index, params: { course_id: course.id }, format: :json + body = JSON.parse(response.body) + expect(body['canManageWeights']).to eq(false) + end + end + end end end diff --git a/spec/models/course/assessment/tab_spec.rb b/spec/models/course/assessment/tab_spec.rb index 6eaea2f517..b546671032 100644 --- a/spec/models/course/assessment/tab_spec.rb +++ b/spec/models/course/assessment/tab_spec.rb @@ -53,5 +53,46 @@ expect(tab).not_to be_valid end end + + describe '.update_gradebook_weights' do + let(:course) { create(:course) } + let(:category) { create(:course_assessment_category, course: course) } + let(:tab1) { create(:course_assessment_tab, category: category) } + let(:tab2) { create(:course_assessment_tab, category: category) } + + it 'updates given tabs' do + described_class.update_gradebook_weights( + course: course, + updates: [{ tab_id: tab1.id, weight: 60 }, { tab_id: tab2.id, weight: 40 }] + ) + expect(tab1.reload.gradebook_weight).to eq(60) + expect(tab2.reload.gradebook_weight).to eq(40) + end + + it 'is transactional — invalid value rolls back everything' do + tab1.update!(gradebook_weight: 10) + tab2.update!(gradebook_weight: 20) + expect do + described_class.update_gradebook_weights( + course: course, + updates: [{ tab_id: tab1.id, weight: 50 }, { tab_id: tab2.id, weight: 999 }] + ) + end.to raise_error(ActiveRecord::RecordInvalid) + expect(tab1.reload.gradebook_weight).to eq(10) + expect(tab2.reload.gradebook_weight).to eq(20) + end + + it 'rejects foreign tab_id' do + other_course = create(:course) + other_tab = create(:course_assessment_tab, + category: create(:course_assessment_category, course: other_course)) + expect do + described_class.update_gradebook_weights( + course: course, + updates: [tab_id: other_tab.id, weight: 50] + ) + end.to raise_error(ActiveRecord::RecordNotFound) + end + end end end From 0c2a9e57e11f0a28edc908505966ee60dff7ed5f Mon Sep 17 00:00:00 2001 From: lws49 Date: Fri, 29 May 2026 12:22:18 +0800 Subject: [PATCH 4/4] =?UTF-8?q?feat(gradebook):=20add=20weighted=20view=20?= =?UTF-8?q?UI=20=E2=80=94=20weights=20table,=20config=20dialog,=20toggle?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - add GradebookWeightedTable with 3-row sticky header showing per-tab weighted subtotals and overall weighted totals per student - add ConfigureWeightsDialog for managers to edit tab weights (0–100), with sum-to-100 warning and integer/NaN validation - add All vs By-weight view toggle in GradebookIndex (role-aware) - add computeWeighted helpers for tab subtotal and student total - store weightedViewEnabled, canManageWeights, and per-tab gradebookWeight from API; wire updateWeights thunk for PATCH /gradebook/weights - add i18n keys and full test coverage for table, dialog, and helpers --- client/app/__test__/mocks/localeMock.js | 2 + client/app/__test__/setup.js | 12 + client/app/api/course/Gradebook.ts | 8 +- .../__tests__/ConfigureWeightsDialog.test.tsx | 80 + .../__tests__/GradebookIndex.test.tsx | 30 + .../__tests__/GradebookWeightedTable.test.tsx | 235 ++ .../__tests__/computeWeighted.test.ts | 133 + .../components/ConfigureWeightsDialog.tsx | 192 + .../components/GradebookWeightedTable.tsx | 336 ++ .../course/gradebook/computeWeighted.ts | 88 + .../bundles/course/gradebook/operations.ts | 8 + .../gradebook/pages/GradebookIndex/index.tsx | 46 +- .../app/bundles/course/gradebook/selectors.ts | 4 + client/app/bundles/course/gradebook/store.ts | 35 +- client/app/bundles/course/gradebook/types.ts | 1 + client/app/types/course/gradebook.ts | 7 + client/jest.config.js | 1 + client/locales/en.json | 3106 +++++++++++------ 18 files changed, 3197 insertions(+), 1127 deletions(-) create mode 100644 client/app/__test__/mocks/localeMock.js create mode 100644 client/app/bundles/course/gradebook/__tests__/ConfigureWeightsDialog.test.tsx create mode 100644 client/app/bundles/course/gradebook/__tests__/GradebookWeightedTable.test.tsx create mode 100644 client/app/bundles/course/gradebook/__tests__/computeWeighted.test.ts create mode 100644 client/app/bundles/course/gradebook/components/ConfigureWeightsDialog.tsx create mode 100644 client/app/bundles/course/gradebook/components/GradebookWeightedTable.tsx create mode 100644 client/app/bundles/course/gradebook/computeWeighted.ts diff --git a/client/app/__test__/mocks/localeMock.js b/client/app/__test__/mocks/localeMock.js new file mode 100644 index 0000000000..1f87539212 --- /dev/null +++ b/client/app/__test__/mocks/localeMock.js @@ -0,0 +1,2 @@ +// File used for jest moduleNameMapper - empty locale messages for tests +module.exports = {}; diff --git a/client/app/__test__/setup.js b/client/app/__test__/setup.js index ce09d53127..d49da129df 100644 --- a/client/app/__test__/setup.js +++ b/client/app/__test__/setup.js @@ -65,3 +65,15 @@ jest.mock('react-router-dom', () => ({ useNavigate: jest.fn(), unstable_usePrompt: jest.fn(), })); + +// Replace I18nProvider with a synchronous stub so tests using test-utils +// don't stall on async locale loading. +jest.mock('lib/components/wrappers/I18nProvider', () => { + const { IntlProvider } = require('react-intl'); + const SyncI18nProvider = ({ children }) => ( + + {children} + + ); + return { __esModule: true, default: SyncI18nProvider }; +}); diff --git a/client/app/api/course/Gradebook.ts b/client/app/api/course/Gradebook.ts index e00c94a64c..7603f1f2a1 100644 --- a/client/app/api/course/Gradebook.ts +++ b/client/app/api/course/Gradebook.ts @@ -1,4 +1,4 @@ -import { GradebookData } from 'types/course/gradebook'; +import { GradebookData, UpdateWeightsPayload } from 'types/course/gradebook'; import { APIResponse } from 'api/types'; @@ -12,4 +12,10 @@ export default class GradebookAPI extends BaseCourseAPI { index(): APIResponse { return this.client.get(this.#urlPrefix); } + + updateWeights( + payload: UpdateWeightsPayload, + ): APIResponse { + return this.client.patch(`${this.#urlPrefix}/weights`, payload); + } } diff --git a/client/app/bundles/course/gradebook/__tests__/ConfigureWeightsDialog.test.tsx b/client/app/bundles/course/gradebook/__tests__/ConfigureWeightsDialog.test.tsx new file mode 100644 index 0000000000..4ad64f3528 --- /dev/null +++ b/client/app/bundles/course/gradebook/__tests__/ConfigureWeightsDialog.test.tsx @@ -0,0 +1,80 @@ +import { fireEvent, render, screen, waitFor } from 'test-utils'; + +import * as operations from '../operations'; +import ConfigureWeightsDialog from '../components/ConfigureWeightsDialog'; + +jest.spyOn(operations, 'updateGradebookWeights').mockReturnValue(async () => {}); + +const categories = [{ id: 1, title: 'Missions' }]; +const tabs = [ + { id: 10, title: 'Assignments', categoryId: 1, gradebookWeight: 50 }, + { id: 11, title: 'Optional', categoryId: 1, gradebookWeight: 50 }, +]; + +const setup = (overrides = {}) => + render( + , + ); + +describe('', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders one input per tab grouped by category', () => { + setup(); + expect(screen.getByText('Missions')).toBeInTheDocument(); + expect(screen.getByLabelText('Assignments')).toHaveValue(50); + expect(screen.getByLabelText('Optional')).toHaveValue(50); + }); + + it('shows Total: 100% with no warning when sum = 100', () => { + setup(); + expect(screen.getByText(/Total:\s*100%/)).toBeInTheDocument(); + expect(screen.queryByText(/do not sum to 100/i)).not.toBeInTheDocument(); + }); + + it('shows warning when sum != 100', () => { + setup(); + fireEvent.change(screen.getByLabelText('Optional'), { target: { value: '30' } }); + expect(screen.getByText(/Total:\s*80%/)).toBeInTheDocument(); + expect(screen.getByText(/do not sum to 100/i)).toBeInTheDocument(); + }); + + it('shows inline error for >100', () => { + setup(); + fireEvent.change(screen.getByLabelText('Assignments'), { target: { value: '101' } }); + expect(screen.getByText(/must be at most 100/i)).toBeInTheDocument(); + }); + + it('shows inline error for negative', () => { + setup(); + fireEvent.change(screen.getByLabelText('Optional'), { target: { value: '-1' } }); + expect(screen.getByText(/must be at least 0/i)).toBeInTheDocument(); + }); + + it('Save dispatches updateGradebookWeights with current values', async () => { + setup(); + fireEvent.change(screen.getByLabelText('Optional'), { target: { value: '40' } }); + fireEvent.click(screen.getByRole('button', { name: /save/i })); + await waitFor(() => { + expect(operations.updateGradebookWeights).toHaveBeenCalledWith([ + { tabId: 10, weight: 50 }, + { tabId: 11, weight: 40 }, + ]); + }); + }); + + it('Cancel does not dispatch', () => { + setup(); + fireEvent.change(screen.getByLabelText('Optional'), { target: { value: '40' } }); + fireEvent.click(screen.getByRole('button', { name: /cancel/i })); + expect(operations.updateGradebookWeights).not.toHaveBeenCalled(); + }); +}); diff --git a/client/app/bundles/course/gradebook/__tests__/GradebookIndex.test.tsx b/client/app/bundles/course/gradebook/__tests__/GradebookIndex.test.tsx index e0fc76ae2c..99d0136431 100644 --- a/client/app/bundles/course/gradebook/__tests__/GradebookIndex.test.tsx +++ b/client/app/bundles/course/gradebook/__tests__/GradebookIndex.test.tsx @@ -72,6 +72,14 @@ const populatedStateWithGamification = { }, }; +const populatedStateWithWeightedView = { + gradebook: { + ...populatedState.gradebook, + weightedViewEnabled: true, + canManageWeights: false, + }, +}; + beforeEach(() => { jest.clearAllMocks(); mockFetchGradebook.mockReturnValue((): Promise => Promise.resolve()); @@ -143,4 +151,26 @@ describe('GradebookIndex', () => { ), ).toBeInTheDocument(); }); + + it('does not render view toggle when weightedViewEnabled is false', async () => { + render(, { state: populatedState }); + // Wait for loading to finish + await screen.findByRole('button', { name: /export/i }); + expect(screen.queryByText(/by weight/i)).not.toBeInTheDocument(); + }); + + it('renders view toggle when weightedViewEnabled is true', async () => { + render(, { state: populatedStateWithWeightedView }); + expect(await screen.findByText(/all assessments/i)).toBeInTheDocument(); + expect(await screen.findByText(/by weight/i)).toBeInTheDocument(); + }); + + it('switches to By weight view on toggle click', async () => { + render(, { state: populatedStateWithWeightedView }); + const byWeightButton = await screen.findByText(/by weight/i); + fireEvent.click(byWeightButton); + expect( + await screen.findByTestId('gradebook-weighted-table'), + ).toBeInTheDocument(); + }); }); diff --git a/client/app/bundles/course/gradebook/__tests__/GradebookWeightedTable.test.tsx b/client/app/bundles/course/gradebook/__tests__/GradebookWeightedTable.test.tsx new file mode 100644 index 0000000000..eddb266d91 --- /dev/null +++ b/client/app/bundles/course/gradebook/__tests__/GradebookWeightedTable.test.tsx @@ -0,0 +1,235 @@ +import { fireEvent } from '@testing-library/react'; +import { render, screen, within } from 'test-utils'; + +import type { + AssessmentData, + CategoryData, + StudentData, + SubmissionData, + TabData, +} from '../types'; +import GradebookWeightedTable from '../components/GradebookWeightedTable'; + +// --------------------------------------------------------------------------- +// Minimal shared fixtures (used where a single tab/category is enough) +// --------------------------------------------------------------------------- +const makeCategory = (id: number, title: string): CategoryData => ({ + id, + title, +}); + +const makeTab = ( + id: number, + title: string, + categoryId: number, + gradebookWeight = 50, +): TabData => ({ id, title, categoryId, gradebookWeight }); + +const makeAssessment = ( + id: number, + title: string, + tabId: number, + maxGrade: number, +): AssessmentData => ({ id, title, tabId, maxGrade }); + +const makeStudent = (id: number, name: string): StudentData => ({ + id, + name, + email: `${name.toLowerCase()}@example.com`, + level: 1, + totalXp: 0, +}); + +const makeSub = ( + studentId: number, + assessmentId: number, + grade: number | null, +): SubmissionData => ({ studentId, assessmentId, grade }); + +// Default render helper +interface RenderWeightedOptions { + categories?: CategoryData[]; + tabs?: TabData[]; + assessments?: AssessmentData[]; + students?: StudentData[]; + submissions?: SubmissionData[]; + canManageWeights?: boolean; + courseTitle?: string; +} + +const renderWeighted = (opts: RenderWeightedOptions = {}): ReturnType => { + const cats = opts.categories ?? [makeCategory(1, 'Cat A')]; + const tabs = opts.tabs ?? [makeTab(10, 'Tab 1', 1, 100)]; + const assessments = opts.assessments ?? [makeAssessment(100, 'Quiz 1', 10, 150)]; + const students = opts.students ?? [makeStudent(1, 'Alice')]; + const submissions = opts.submissions ?? []; + return render( + , + ); +}; + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- +describe('GradebookWeightedTable', () => { + // 1. Row 1: category cells with colSpan + it('renders category cells in row 1 with colSpan equal to number of tabs in that category', () => { + const cats = [makeCategory(1, 'Cat A'), makeCategory(2, 'Cat B')]; + const tabs = [ + makeTab(10, 'Tab 1', 1, 50), + makeTab(11, 'Tab 2', 1, 50), + makeTab(20, 'Tab 3', 2, 0), + ]; + const assessments = [ + makeAssessment(100, 'Q1', 10, 10), + makeAssessment(101, 'Q2', 11, 10), + makeAssessment(102, 'Q3', 20, 10), + ]; + renderWeighted({ categories: cats, tabs, assessments }); + const thead = document.querySelector('thead')!; + const rows = thead.querySelectorAll('tr'); + const row1Cells = rows[0].querySelectorAll('th'); + // We expect a cell for "Cat A" (colSpan=2) and a cell for "Cat B" (colSpan=1) + const catACell = Array.from(row1Cells).find( + (c) => c.textContent === 'Cat A', + ); + const catBCell = Array.from(row1Cells).find( + (c) => c.textContent === 'Cat B', + ); + expect(catACell).toBeTruthy(); + expect(catBCell).toBeTruthy(); + expect(catACell!.getAttribute('colspan') ?? catACell!.colSpan.toString()).toBe('2'); + expect(catBCell!.getAttribute('colspan') ?? catBCell!.colSpan.toString()).toBe('1'); + }); + + // 2. Row 2: tab title cells + it('renders tab title cells in row 2', () => { + renderWeighted({ + tabs: [makeTab(10, 'Homework', 1, 60), makeTab(11, 'Exams', 1, 40)], + }); + const thead = document.querySelector('thead')!; + const row2 = thead.querySelectorAll('tr')[1]; + expect(within(row2 as HTMLElement).getByText('Homework')).toBeInTheDocument(); + expect(within(row2 as HTMLElement).getByText('Exams')).toBeInTheDocument(); + }); + + // 3. Weight subheader shows "X% of grade" per tab + it('shows weight subheader "X% of grade" for each tab in row 3', () => { + renderWeighted({ + tabs: [makeTab(10, 'Tab 1', 1, 30), makeTab(11, 'Tab 2', 1, 70)], + }); + expect(screen.getByText('30% of grade')).toBeInTheDocument(); + expect(screen.getByText('70% of grade')).toBeInTheDocument(); + }); + + // 4a. Total column shows "100% total" when sum = 100 + it('shows "100% total" in total column header when weights sum to 100', () => { + renderWeighted({ + tabs: [makeTab(10, 'Tab 1', 1, 60), makeTab(11, 'Tab 2', 1, 40)], + }); + expect(screen.getByText('100% total')).toBeInTheDocument(); + }); + + // 4b. Total column shows warning text when sum ≠ 100 + it('shows a warning when weight sum ≠ 100 in total header', () => { + renderWeighted({ + tabs: [makeTab(10, 'Tab 1', 1, 30), makeTab(11, 'Tab 2', 1, 30)], + }); + // The sum is 60, so there should be a warning indicated in the header + expect(screen.getByText(/60%/)).toBeInTheDocument(); + expect(screen.getByText(/does not sum to 100/i)).toBeInTheDocument(); + }); + + // 5. Student subtotal shows computed percentage "86.67%" + it('renders student subtotal as computed percentage', () => { + // grade=130, maxGrade=150 → 130/150 = 0.86666... → 86.67% + renderWeighted({ + tabs: [makeTab(10, 'Tab 1', 1, 100)], + assessments: [makeAssessment(100, 'Q1', 10, 150)], + students: [makeStudent(1, 'Alice')], + submissions: [makeSub(1, 100, 130)], + }); + // Both subtotal and total show 86.67% (single tab with weight=100) + expect(screen.getAllByText('86.67%').length).toBeGreaterThanOrEqual(1); + }); + + // 6. Tab with no assessments → subtotal "—" + it('shows "—" for a tab with no assessments', () => { + renderWeighted({ + tabs: [makeTab(10, 'Empty Tab', 1, 100)], + assessments: [], // no assessments in this tab + students: [makeStudent(1, 'Alice')], + submissions: [], + }); + expect(screen.getAllByText('—').length).toBeGreaterThanOrEqual(1); + }); + + // 7. Student with no graded submissions in weighted tab → subtotal "—" + it('shows "—" when student has no graded submissions in a tab', () => { + renderWeighted({ + tabs: [makeTab(10, 'Tab 1', 1, 100)], + assessments: [makeAssessment(100, 'Q1', 10, 10)], + students: [makeStudent(1, 'Alice')], + submissions: [], // no submissions at all + }); + expect(screen.getAllByText('—').length).toBeGreaterThanOrEqual(1); + }); + + // 8. Treat Ungraded as 0 toggle changes numbers + it('changes subtotal values when "Treat Ungraded as 0" is toggled', () => { + renderWeighted({ + tabs: [makeTab(10, 'Tab 1', 1, 100)], + assessments: [ + makeAssessment(100, 'Q1', 10, 50), + makeAssessment(101, 'Q2', 10, 50), + ], + students: [makeStudent(1, 'Alice')], + // Alice graded on Q1=40, Q2 ungraded + submissions: [makeSub(1, 100, 40)], + }); + + // Without toggle: only Q1 is graded → 40/50 = 80.00% + expect(screen.getAllByText('80.00%').length).toBeGreaterThanOrEqual(1); + + // Toggle on "Treat Ungraded as 0" + const toggle = screen.getByRole('checkbox', { name: /treat ungraded as 0/i }); + fireEvent.click(toggle); + + // With toggle: 40/(50+50) = 40.00% + expect(screen.getAllByText('40.00%').length).toBeGreaterThanOrEqual(1); + }); + + // 9. All weights zero → empty-state banner visible + it('shows empty-state banner when all weights are 0', () => { + renderWeighted({ + tabs: [makeTab(10, 'Tab 1', 1, 0), makeTab(11, 'Tab 2', 1, 0)], + }); + expect( + screen.getByText(/no weights configured/i), + ).toBeInTheDocument(); + }); + + // 10. canManageWeights === false → no "Configure Weights" button + it('does not show Configure Weights button when canManageWeights is false', () => { + renderWeighted({ canManageWeights: false }); + expect( + screen.queryByRole('button', { name: /configure weights/i }), + ).not.toBeInTheDocument(); + }); + + // 11. canManageWeights === true → "Configure Weights" button present + it('shows Configure Weights button when canManageWeights is true', () => { + renderWeighted({ canManageWeights: true }); + expect( + screen.getByRole('button', { name: /configure weights/i }), + ).toBeInTheDocument(); + }); +}); diff --git a/client/app/bundles/course/gradebook/__tests__/computeWeighted.test.ts b/client/app/bundles/course/gradebook/__tests__/computeWeighted.test.ts new file mode 100644 index 0000000000..d2488e76d2 --- /dev/null +++ b/client/app/bundles/course/gradebook/__tests__/computeWeighted.test.ts @@ -0,0 +1,133 @@ +// client/app/bundles/course/gradebook/__tests__/computeWeighted.test.ts +import { computeTabSubtotal, computeStudentTotal } from '../computeWeighted'; + +const assessments = [ + { id: 1, tabId: 10, maxGrade: 100, title: 'A' }, + { id: 2, tabId: 10, maxGrade: 50, title: 'B' }, + { id: 3, tabId: 20, maxGrade: 100, title: 'C' }, +]; + +const subs = (entries: { studentId: number; assessmentId: number; grade: number | null }[]) => + entries; + +describe('computeTabSubtotal', () => { + it('returns null when tab has no assessments', () => { + expect( + computeTabSubtotal({ + studentId: 1, + tab: { id: 999, title: 'X', categoryId: 0 }, + assessments, + submissions: [], + treatUngradedAsZero: false, + }), + ).toBeNull(); + }); + + it('returns null when student has no graded submissions and toggle off', () => { + expect( + computeTabSubtotal({ + studentId: 1, + tab: { id: 10, title: 'M', categoryId: 0 }, + assessments, + submissions: [], + treatUngradedAsZero: false, + }), + ).toBeNull(); + }); + + it('sum-of-points across graded only when toggle off', () => { + expect( + computeTabSubtotal({ + studentId: 1, + tab: { id: 10, title: 'M', categoryId: 0 }, + assessments, + submissions: subs([ + { studentId: 1, assessmentId: 1, grade: 80 }, + // assessment 2 ungraded + ]), + treatUngradedAsZero: false, + }), + ).toBeCloseTo(0.8); + }); + + it('includes ungraded as zero when toggle on', () => { + expect( + computeTabSubtotal({ + studentId: 1, + tab: { id: 10, title: 'M', categoryId: 0 }, + assessments, + submissions: subs([{ studentId: 1, assessmentId: 1, grade: 80 }]), + treatUngradedAsZero: true, + }), + ).toBeCloseTo(80 / 150); + }); +}); + +describe('computeStudentTotal', () => { + const tabs = [ + { id: 10, title: 'M', categoryId: 0, gradebookWeight: 60 }, + { id: 20, title: 'T', categoryId: 0, gradebookWeight: 40 }, + ]; + + it('weighted average over weighted tabs', () => { + const total = computeStudentTotal({ + studentId: 1, + tabs, + assessments, + submissions: subs([ + { studentId: 1, assessmentId: 1, grade: 80 }, + { studentId: 1, assessmentId: 2, grade: 50 }, + { studentId: 1, assessmentId: 3, grade: 90 }, + ]), + treatUngradedAsZero: false, + }); + expect(total).toBeCloseTo((60 * (130 / 150) + 40 * 0.9) / 100); + }); + + it('excludes tabs with weight 0', () => { + const total = computeStudentTotal({ + studentId: 1, + tabs: [ + { id: 10, title: 'M', categoryId: 0, gradebookWeight: 100 }, + { id: 20, title: 'T', categoryId: 0, gradebookWeight: 0 }, + ], + assessments, + submissions: subs([ + { studentId: 1, assessmentId: 1, grade: 80 }, + { studentId: 1, assessmentId: 2, grade: 50 }, + ]), + treatUngradedAsZero: false, + }); + expect(total).toBeCloseTo(130 / 150); + }); + + it('returns null when no weighted tab contributes', () => { + expect( + computeStudentTotal({ + studentId: 1, + tabs: [{ id: 10, title: 'M', categoryId: 0, gradebookWeight: 0 }], + assessments, + submissions: [], + treatUngradedAsZero: false, + }), + ).toBeNull(); + }); + + it('normalizes when weights do not sum to 100', () => { + const total = computeStudentTotal({ + studentId: 1, + tabs: [ + { id: 10, title: 'M', categoryId: 0, gradebookWeight: 60 }, + { id: 20, title: 'T', categoryId: 0, gradebookWeight: 30 }, + ], + assessments, + submissions: subs([ + { studentId: 1, assessmentId: 1, grade: 80 }, + { studentId: 1, assessmentId: 2, grade: 50 }, + { studentId: 1, assessmentId: 3, grade: 90 }, + ]), + treatUngradedAsZero: false, + }); + expect(total).toBeCloseTo((60 * (130 / 150) + 30 * 0.9) / 90); + }); +}); diff --git a/client/app/bundles/course/gradebook/components/ConfigureWeightsDialog.tsx b/client/app/bundles/course/gradebook/components/ConfigureWeightsDialog.tsx new file mode 100644 index 0000000000..2740d05ee3 --- /dev/null +++ b/client/app/bundles/course/gradebook/components/ConfigureWeightsDialog.tsx @@ -0,0 +1,192 @@ +import { FC, useEffect, useState } from 'react'; +import { defineMessages } from 'react-intl'; +import { + Alert, + Button, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + Stack, + TextField, + Typography, +} from '@mui/material'; +import type { CategoryData, TabData } from 'types/course/gradebook'; + +import { useAppDispatch } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; +import useTranslation from 'lib/hooks/useTranslation'; + +import { updateGradebookWeights } from '../operations'; + +const translations = defineMessages({ + dialogTitle: { + id: 'course.gradebook.ConfigureWeightsDialog.dialogTitle', + defaultMessage: 'Configure tab weights', + }, + description: { + id: 'course.gradebook.ConfigureWeightsDialog.description', + defaultMessage: + 'Set how much each tab contributes to the total grade. Weights should sum to 100.', + }, + total: { + id: 'course.gradebook.ConfigureWeightsDialog.total', + defaultMessage: 'Total: {sum}%', + }, + weightsDoNotSum: { + id: 'course.gradebook.ConfigureWeightsDialog.weightsDoNotSum', + defaultMessage: + 'Weights do not sum to 100. Saving is allowed; Total may be inaccurate.', + }, + cancel: { + id: 'course.gradebook.ConfigureWeightsDialog.cancel', + defaultMessage: 'Cancel', + }, + save: { + id: 'course.gradebook.ConfigureWeightsDialog.save', + defaultMessage: 'Save', + }, + valueTooLow: { + id: 'course.gradebook.ConfigureWeightsDialog.valueTooLow', + defaultMessage: 'Value must be at least 0', + }, + valueTooHigh: { + id: 'course.gradebook.ConfigureWeightsDialog.valueTooHigh', + defaultMessage: 'Value must be at most 100', + }, + valueNotInteger: { + id: 'course.gradebook.ConfigureWeightsDialog.valueNotInteger', + defaultMessage: 'Value must be a whole number', + }, + saveError: { + id: 'course.gradebook.ConfigureWeightsDialog.saveError', + defaultMessage: 'Failed to save weights. Please try again.', + }, +}); + +interface Props { + open: boolean; + onClose: () => void; + categories: CategoryData[]; + tabs: TabData[]; +} + +const ConfigureWeightsDialog: FC = ({ + open, + onClose, + categories, + tabs, +}) => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + + const validate = (value: number): string | null => { + if (isNaN(value)) return t(translations.valueTooLow); + if (!Number.isInteger(value)) return t(translations.valueNotInteger); + if (value < 0) return t(translations.valueTooLow); + if (value > 100) return t(translations.valueTooHigh); + return null; + }; + + const [weights, setWeights] = useState>(() => + Object.fromEntries(tabs.map((tb) => [tb.id, tb.gradebookWeight ?? 0])), + ); + const [submitting, setSubmitting] = useState(false); + + useEffect(() => { + if (open) { + setWeights( + Object.fromEntries(tabs.map((tb) => [tb.id, tb.gradebookWeight ?? 0])), + ); + } + }, [open]); // eslint-disable-line react-hooks/exhaustive-deps + + const sum = Object.values(weights).reduce((acc, w) => acc + w, 0); + const hasInvalid = Object.values(weights).some((w) => validate(w) !== null); + + const handleChange = (tabId: number, raw: string): void => { + const parsed = raw === '' ? 0 : Number(raw); + setWeights((prev) => ({ ...prev, [tabId]: parsed })); + }; + + const handleSave = async (): Promise => { + if (hasInvalid) return; + setSubmitting(true); + try { + await dispatch( + updateGradebookWeights( + tabs.map((tb) => ({ tabId: tb.id, weight: weights[tb.id] ?? 0 })), + ), + ); + onClose(); + } catch { + toast.error(t(translations.saveError)); + } finally { + setSubmitting(false); + } + }; + + return ( + + {t(translations.dialogTitle)} + + + {t(translations.description)} + + + {categories.map((cat) => ( +
+ {cat.title} + + {tabs + .filter((tb) => tb.categoryId === cat.id) + .map((tb) => { + const value = weights[tb.id] ?? 0; + const err = validate(value); + return ( + handleChange(tb.id, e.target.value)} + size="small" + type="number" + value={value} + /> + ); + })} + +
+ ))} +
+ + {t(translations.total, { sum })} + + {sum !== 100 && ( + + {t(translations.weightsDoNotSum)} + + )} +
+ + + + +
+ ); +}; + +export default ConfigureWeightsDialog; diff --git a/client/app/bundles/course/gradebook/components/GradebookWeightedTable.tsx b/client/app/bundles/course/gradebook/components/GradebookWeightedTable.tsx new file mode 100644 index 0000000000..bde4ccc91a --- /dev/null +++ b/client/app/bundles/course/gradebook/components/GradebookWeightedTable.tsx @@ -0,0 +1,336 @@ +import { useMemo, useState } from 'react'; +import { defineMessages } from 'react-intl'; +import { + Alert, + Button, + FormControlLabel, + Paper, + Switch, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Tooltip, + Typography, +} from '@mui/material'; +import type { + AssessmentData, + CategoryData, + StudentData, + SubmissionData, + TabData, +} from 'types/course/gradebook'; + +import useTranslation from 'lib/hooks/useTranslation'; + +import { + computeStudentTotal, + computeTabSubtotal, + sumWeights, +} from '../computeWeighted'; + +import ConfigureWeightsDialog from './ConfigureWeightsDialog'; + +const translations = defineMessages({ + treatUngradedAsZero: { + id: 'course.gradebook.GradebookWeightedTable.treatUngradedAsZero', + defaultMessage: 'Treat Ungraded as 0', + }, + configureWeights: { + id: 'course.gradebook.GradebookWeightedTable.configureWeights', + defaultMessage: 'Configure Weights', + }, + noWeightsConfigured: { + id: 'course.gradebook.GradebookWeightedTable.noWeightsConfigured', + defaultMessage: + 'No weights configured — all tab weights are 0. Click "Configure Weights" to assign weights.', + }, + noWeightsNoAccess: { + id: 'course.gradebook.GradebookWeightedTable.noWeightsNoAccess', + defaultMessage: 'No tab weights have been configured yet.', + }, + student: { + id: 'course.gradebook.GradebookWeightedTable.student', + defaultMessage: 'Student', + }, + total: { + id: 'course.gradebook.GradebookWeightedTable.total', + defaultMessage: 'Total', + }, + percentOfGrade: { + id: 'course.gradebook.GradebookWeightedTable.percentOfGrade', + defaultMessage: '{weight}% of grade', + }, + percentTotalExact: { + id: 'course.gradebook.GradebookWeightedTable.percentTotalExact', + defaultMessage: '100% total', + }, + percentTotalWarning: { + id: 'course.gradebook.GradebookWeightedTable.percentTotalWarning', + defaultMessage: '{weight}% total', + }, + doesNotSumTo100: { + id: 'course.gradebook.GradebookWeightedTable.doesNotSumTo100', + defaultMessage: 'does not sum to 100', + }, + weightsDoNotSum: { + id: 'course.gradebook.GradebookWeightedTable.weightsDoNotSum', + defaultMessage: 'Weights do not sum to 100. Total may be inaccurate.', + }, +}); + +interface Props { + categories: CategoryData[]; + tabs: TabData[]; + assessments: AssessmentData[]; + students: StudentData[]; + submissions: SubmissionData[]; + canManageWeights: boolean; +} + +const fmt = (v: number | null): string => { + if (v === null) return '—'; + return `${(v * 100).toFixed(2)}%`; +}; + +const GradebookWeightedTable = ({ + categories, + tabs, + assessments, + students, + submissions, + canManageWeights, +}: Props): JSX.Element => { + const { t } = useTranslation(); + const [treatUngradedAsZero, setTreatUngradedAsZero] = useState(false); + const [configureOpen, setConfigureOpen] = useState(false); + + const totalWeight = sumWeights(tabs); + const allWeightsZero = totalWeight === 0; + + // Build per-category tab counts for row 1 colSpans + const categoryTabCounts = useMemo(() => { + const counts = new Map(); + tabs.forEach((tab) => { + counts.set(tab.categoryId, (counts.get(tab.categoryId) ?? 0) + 1); + }); + return counts; + }, [tabs]); + + // Ordered categories that have at least one tab + const visibleCategories = useMemo( + () => categories.filter((cat) => categoryTabCounts.has(cat.id)), + [categories, categoryTabCounts], + ); + + // Compute per-student rows + const rowData = useMemo( + () => + students.map((student) => { + const subtotals = tabs.map((tab) => + computeTabSubtotal({ + studentId: student.id, + tab, + assessments, + submissions, + treatUngradedAsZero, + }), + ); + const total = computeStudentTotal({ + studentId: student.id, + tabs, + assessments, + submissions, + treatUngradedAsZero, + }); + return { student, subtotals, total }; + }), + [students, tabs, assessments, submissions, treatUngradedAsZero], + ); + + return ( +
+ {/* Toolbar */} +
+ setTreatUngradedAsZero(e.target.checked)} + size="small" + /> + } + label={t(translations.treatUngradedAsZero)} + /> + {canManageWeights && ( + + )} +
+ + {/* Empty state banner when all weights are 0 */} + {allWeightsZero && ( +
+ + {canManageWeights + ? t(translations.noWeightsConfigured) + : t(translations.noWeightsNoAccess)} + +
+ )} + + {/* Table */} +
+ + + ({ + tableLayout: 'auto', + borderCollapse: 'separate', + borderSpacing: 0, + '& th, & td': { + boxSizing: 'border-box', + border: 0, + borderBottom: `0.5px solid ${theme.palette.grey[200]}`, + }, + })} + > + + {/* Row 1: Categories */} + + {/* Student name column header */} + + {t(translations.student)} + + {visibleCategories.map((cat) => ( + + {cat.title} + + ))} + {/* Total column */} + + {t(translations.total)} + + + + {/* Row 2: Tab titles */} + + {tabs.map((tab) => ( + + {tab.title} + + ))} + + + {/* Row 3: Weight subheaders */} + + {tabs.map((tab) => ( + + {t(translations.percentOfGrade, { + weight: tab.gradebookWeight ?? 0, + })} + + ))} + {/* Total weight cell */} + + {totalWeight === 100 ? ( + t(translations.percentTotalExact) + ) : ( + + + + {t(translations.percentTotalWarning, { + weight: totalWeight, + })} + + + {t(translations.doesNotSumTo100)} + + + + )} + + + + + + {rowData.map(({ student, subtotals, total }) => ( + + {student.name} + {subtotals.map((subtotal, idx) => ( + + {fmt(subtotal)} + + ))} + {fmt(total)} + + ))} + +
+
+
+
+ + {/* Configure Weights Dialog */} + {canManageWeights && ( + setConfigureOpen(false)} + open={configureOpen} + tabs={tabs} + /> + )} +
+ ); +}; + +export default GradebookWeightedTable; diff --git a/client/app/bundles/course/gradebook/computeWeighted.ts b/client/app/bundles/course/gradebook/computeWeighted.ts new file mode 100644 index 0000000000..839bd7dc3d --- /dev/null +++ b/client/app/bundles/course/gradebook/computeWeighted.ts @@ -0,0 +1,88 @@ +// client/app/bundles/course/gradebook/computeWeighted.ts +import { + AssessmentData, + SubmissionData, + TabData, +} from 'types/course/gradebook'; + +interface SubtotalArgs { + studentId: number; + tab: TabData; + assessments: AssessmentData[]; + submissions: SubmissionData[]; + treatUngradedAsZero: boolean; +} + +export const computeTabSubtotal = ({ + studentId, + tab, + assessments, + submissions, + treatUngradedAsZero, +}: SubtotalArgs): number | null => { + const tabAssessments = assessments.filter((a) => a.tabId === tab.id); + if (tabAssessments.length === 0) return null; + + const { numerator, denominator } = tabAssessments.reduce( + (acc, a) => { + const grade = submissions.find( + (s) => s.studentId === studentId && s.assessmentId === a.id, + )?.grade; + if (grade != null) { + return { + numerator: acc.numerator + grade, + denominator: acc.denominator + a.maxGrade, + }; + } + if (treatUngradedAsZero) { + return { + numerator: acc.numerator, + denominator: acc.denominator + a.maxGrade, + }; + } + return acc; + }, + { numerator: 0, denominator: 0 }, + ); + return denominator > 0 ? numerator / denominator : null; +}; + +interface TotalArgs { + studentId: number; + tabs: TabData[]; + assessments: AssessmentData[]; + submissions: SubmissionData[]; + treatUngradedAsZero: boolean; +} + +export const computeStudentTotal = ({ + studentId, + tabs, + assessments, + submissions, + treatUngradedAsZero, +}: TotalArgs): number | null => { + const { weightedSum, weightSum } = tabs.reduce( + (acc, tab) => { + const weight = tab.gradebookWeight ?? 0; + if (weight <= 0) return acc; + const sub = computeTabSubtotal({ + studentId, + tab, + assessments, + submissions, + treatUngradedAsZero, + }); + if (sub == null) return acc; + return { + weightedSum: acc.weightedSum + weight * sub, + weightSum: acc.weightSum + weight, + }; + }, + { weightedSum: 0, weightSum: 0 }, + ); + return weightSum > 0 ? weightedSum / weightSum : null; +}; + +export const sumWeights = (tabs: TabData[]): number => + tabs.reduce((acc, t) => acc + (t.gradebookWeight ?? 0), 0); diff --git a/client/app/bundles/course/gradebook/operations.ts b/client/app/bundles/course/gradebook/operations.ts index 35790580ed..ae2f962fbb 100644 --- a/client/app/bundles/course/gradebook/operations.ts +++ b/client/app/bundles/course/gradebook/operations.ts @@ -1,4 +1,5 @@ import type { Operation } from 'store'; +import type { UpdateWeightsPayload } from 'types/course/gradebook'; import CourseAPI from 'api/course'; @@ -9,4 +10,11 @@ const fetchGradebook = (): Operation => async (dispatch) => { dispatch(actions.saveGradebook(response.data)); }; +export const updateGradebookWeights = + (weights: UpdateWeightsPayload['weights']): Operation => + async (dispatch) => { + const response = await CourseAPI.gradebook.updateWeights({ weights }); + dispatch(actions.updateTabWeights(response.data)); + }; + export default fetchGradebook; diff --git a/client/app/bundles/course/gradebook/pages/GradebookIndex/index.tsx b/client/app/bundles/course/gradebook/pages/GradebookIndex/index.tsx index cc140af58f..a437f65ec8 100644 --- a/client/app/bundles/course/gradebook/pages/GradebookIndex/index.tsx +++ b/client/app/bundles/course/gradebook/pages/GradebookIndex/index.tsx @@ -2,7 +2,7 @@ import { FC, useEffect, useState } from 'react'; import { defineMessages } from 'react-intl'; import { useParams } from 'react-router-dom'; import { PeopleAlt } from '@mui/icons-material'; -import { Typography } from '@mui/material'; +import { ToggleButton, ToggleButtonGroup, Typography } from '@mui/material'; import Page from 'lib/components/core/layouts/Page'; import LoadingIndicator from 'lib/components/core/LoadingIndicator'; @@ -12,14 +12,17 @@ import useTranslation from 'lib/hooks/useTranslation'; import { useCourseContext } from '../../../container/CourseLoader'; import GradebookTable from '../../components/GradebookTable'; +import GradebookWeightedTable from '../../components/GradebookWeightedTable'; import fetchGradebook from '../../operations'; import { getAssessments, + getCanManageWeights, getCategories, getGamificationEnabled, getStudents, getSubmissions, getTabs, + getWeightedViewEnabled, } from '../../selectors'; const translations = defineMessages({ @@ -39,6 +42,14 @@ const translations = defineMessages({ id: 'course.gradebook.GradebookIndex.noStudentsHint', defaultMessage: 'Grades will appear here once students join the course.', }, + allAssessments: { + id: 'course.gradebook.GradebookIndex.allAssessments', + defaultMessage: 'All assessments', + }, + byWeight: { + id: 'course.gradebook.GradebookIndex.byWeight', + defaultMessage: 'By weight', + }, }); const GradebookIndex: FC = () => { @@ -48,6 +59,7 @@ const GradebookIndex: FC = () => { const { courseId: courseIdParam } = useParams(); const courseId = parseInt(courseIdParam!, 10); const [isLoading, setIsLoading] = useState(true); + const [viewMode, setViewMode] = useState<'all' | 'weighted'>('all'); const assessments = useAppSelector(getAssessments); const categories = useAppSelector(getCategories); @@ -55,6 +67,8 @@ const GradebookIndex: FC = () => { const students = useAppSelector(getStudents); const submissions = useAppSelector(getSubmissions); const gamificationEnabled = useAppSelector(getGamificationEnabled); + const weightedViewEnabled = useAppSelector(getWeightedViewEnabled); + const canManageWeights = useAppSelector(getCanManageWeights); useEffect(() => { dispatch(fetchGradebook()) @@ -77,6 +91,17 @@ const GradebookIndex: FC = () => { ); + } else if (weightedViewEnabled && viewMode === 'weighted') { + content = ( + + ); } else { content = ( { return ( + {weightedViewEnabled && !isLoading && students.length > 0 && ( +
+ { + if (v) setViewMode(v); + }} + size="small" + value={viewMode} + > + + {t(translations.allAssessments)} + + + {t(translations.byWeight)} + + +
+ )} {content}
); diff --git a/client/app/bundles/course/gradebook/selectors.ts b/client/app/bundles/course/gradebook/selectors.ts index fbe62e2611..0fb7d1398d 100644 --- a/client/app/bundles/course/gradebook/selectors.ts +++ b/client/app/bundles/course/gradebook/selectors.ts @@ -22,3 +22,7 @@ export const getGamificationEnabled = ( state: AppState, ): GradebookState['gamificationEnabled'] => getLocalState(state).gamificationEnabled; +export const getWeightedViewEnabled = (state: AppState): boolean => + getLocalState(state).weightedViewEnabled; +export const getCanManageWeights = (state: AppState): boolean => + getLocalState(state).canManageWeights; diff --git a/client/app/bundles/course/gradebook/store.ts b/client/app/bundles/course/gradebook/store.ts index 00e3291032..0c6f4d1a07 100644 --- a/client/app/bundles/course/gradebook/store.ts +++ b/client/app/bundles/course/gradebook/store.ts @@ -1,5 +1,8 @@ import { produce } from 'immer'; -import type { GradebookData } from 'types/course/gradebook'; +import type { + GradebookData, + UpdateWeightsPayload, +} from 'types/course/gradebook'; import type { AssessmentData, @@ -10,6 +13,7 @@ import type { } from './types'; const SAVE_GRADEBOOK = 'course/gradebook/SAVE_GRADEBOOK'; +const UPDATE_TAB_WEIGHTS = 'course/gradebook/UPDATE_TAB_WEIGHTS'; interface GradebookState { categories: CategoryData[]; @@ -18,6 +22,8 @@ interface GradebookState { students: StudentData[]; submissions: SubmissionData[]; gamificationEnabled: boolean; + weightedViewEnabled: boolean; + canManageWeights: boolean; } interface SaveGradebookAction { @@ -25,6 +31,11 @@ interface SaveGradebookAction { payload: GradebookData; } +interface UpdateTabWeightsAction { + type: typeof UPDATE_TAB_WEIGHTS; + payload: UpdateWeightsPayload; +} + const initialState: GradebookState = { categories: [], tabs: [], @@ -32,10 +43,15 @@ const initialState: GradebookState = { students: [], submissions: [], gamificationEnabled: false, + weightedViewEnabled: false, + canManageWeights: false, }; const reducer = produce( - (draft: GradebookState, action: SaveGradebookAction) => { + ( + draft: GradebookState, + action: SaveGradebookAction | UpdateTabWeightsAction, + ) => { switch (action.type) { case SAVE_GRADEBOOK: { draft.categories = action.payload.categories; @@ -44,6 +60,15 @@ const reducer = produce( draft.students = action.payload.students; draft.submissions = action.payload.submissions; draft.gamificationEnabled = action.payload.gamificationEnabled; + draft.weightedViewEnabled = action.payload.weightedViewEnabled; + draft.canManageWeights = action.payload.canManageWeights; + break; + } + case UPDATE_TAB_WEIGHTS: { + action.payload.weights.forEach(({ tabId, weight }) => { + const tab = draft.tabs.find((t) => t.id === tabId); + if (tab) tab.gradebookWeight = weight; + }); break; } default: @@ -58,6 +83,12 @@ export const actions = { type: SAVE_GRADEBOOK, payload: data, }), + updateTabWeights: ( + payload: UpdateWeightsPayload, + ): UpdateTabWeightsAction => ({ + type: UPDATE_TAB_WEIGHTS, + payload, + }), }; export default reducer; diff --git a/client/app/bundles/course/gradebook/types.ts b/client/app/bundles/course/gradebook/types.ts index f94aa7bf9c..b91689df87 100644 --- a/client/app/bundles/course/gradebook/types.ts +++ b/client/app/bundles/course/gradebook/types.ts @@ -5,4 +5,5 @@ export type { StudentData, SubmissionData, TabData, + UpdateWeightsPayload, } from 'types/course/gradebook'; diff --git a/client/app/types/course/gradebook.ts b/client/app/types/course/gradebook.ts index 613df6b1cd..d041add93b 100644 --- a/client/app/types/course/gradebook.ts +++ b/client/app/types/course/gradebook.ts @@ -7,6 +7,7 @@ export interface TabData { id: number; title: string; categoryId: number; + gradebookWeight?: number; } export interface AssessmentData { @@ -37,4 +38,10 @@ export interface GradebookData { students: StudentData[]; submissions: SubmissionData[]; gamificationEnabled: boolean; + weightedViewEnabled: boolean; + canManageWeights: boolean; +} + +export interface UpdateWeightsPayload { + weights: { tabId: number; weight: number }[]; } diff --git a/client/jest.config.js b/client/jest.config.js index 947fb08889..38d6c43835 100644 --- a/client/jest.config.js +++ b/client/jest.config.js @@ -24,6 +24,7 @@ const config = { '^course(.*)$': '/app/bundles/course$1', '^store(.*)$': '/app/store$1', '^lodash-es(.*)$': 'lodash$1', + 'compiled-locales/.*\\.json$': '/app/__test__/mocks/localeMock.js', }, testPathIgnorePatterns: ['/node_modules/', '/dist/'], coveragePathIgnorePatterns: ['/node_modules/', '/__test__/'], diff --git a/client/locales/en.json b/client/locales/en.json index e18c2303d0..7a80547f84 100644 --- a/client/locales/en.json +++ b/client/locales/en.json @@ -1,4 +1,7 @@ { + "2kgbTg": { + "defaultMessage": "Role-Playing Assessment" + }, "announcements.GlobalAnnouncementIndex.fetchAnnouncementsFailure": { "defaultMessage": "Unable to fetch announcements" }, @@ -29,6 +32,9 @@ "app.DashboardPage.yourCourses": { "defaultMessage": "Your Courses" }, + "app.ErrorPage.courseSuspended": { + "defaultMessage": "This course is suspended." + }, "app.ErrorPage.error": { "defaultMessage": "KABOOM, a meteor has just crashed." }, @@ -59,14 +65,11 @@ "app.ErrorPage.notFoundSubtitle": { "defaultMessage": "Check if you've typed the correct address, try again later, or go back home." }, - "app.ErrorPage.userSuspended": { - "defaultMessage": "Your access to this course has been suspended." - }, "app.ErrorPage.suspendedSubtitle": { "defaultMessage": "Please contact your instructors or the course staff." }, - "app.ErrorPage.courseSuspended": { - "defaultMessage": "This course is suspended." + "app.ErrorPage.userSuspended": { + "defaultMessage": "Your access to this course has been suspended." }, "app.Footer.contactUs": { "defaultMessage": "Contact Us" @@ -80,12 +83,12 @@ "app.Footer.instructorsGuide": { "defaultMessage": "Instructors' Guide" }, - "app.Footer.reportIssue": { - "defaultMessage": "Report an Issue" - }, "app.Footer.privacyPolicy": { "defaultMessage": "Privacy Policy" }, + "app.Footer.reportIssue": { + "defaultMessage": "Report an Issue" + }, "app.Footer.termsOfService": { "defaultMessage": "Terms of Service" }, @@ -101,6 +104,9 @@ "assessment.attemptLoader.errorAttemptingAssessment": { "defaultMessage": "An error occurred while attempting this assessment. Try again later." }, + "bW7B87": { + "defaultMessage": "New" + }, "client.video.attemptLoader.errorWatchVideo": { "defaultMessage": "An error occurred while attempting to watch this video. Try again later." }, @@ -263,23 +269,23 @@ "course.achievement.AchievementAward.AchievementAwardManager.saveChanges": { "defaultMessage": "Save Changes" }, + "course.achievement.AchievementAward.AchievementAwardSummary.awardedStudents": { + "defaultMessage": "Awarded Students" + }, "course.achievement.AchievementAward.AchievementAwardSummary.name": { "defaultMessage": "Name" }, - "course.achievement.AchievementAward.AchievementAwardSummary.userType": { - "defaultMessage": "User Type" + "course.achievement.AchievementAward.AchievementAwardSummary.normalStudent": { + "defaultMessage": "Normal Student" }, - "course.achievement.AchievementAward.AchievementAwardSummary.awardedStudents": { - "defaultMessage": "Awarded Students" + "course.achievement.AchievementAward.AchievementAwardSummary.phantomStudent": { + "defaultMessage": "Phantom Student" }, "course.achievement.AchievementAward.AchievementAwardSummary.revokedStudents": { "defaultMessage": "Revoked Students" }, - "course.achievement.AchievementAward.AchievementAwardSummary.phantomStudent": { - "defaultMessage": "Phantom Student" - }, - "course.achievement.AchievementAward.AchievementAwardSummary.normalStudent": { - "defaultMessage": "Normal Student" + "course.achievement.AchievementAward.AchievementAwardSummary.userType": { + "defaultMessage": "User Type" }, "course.achievement.AchievementAward.awardAchievement": { "defaultMessage": "Award Achievement" @@ -353,26 +359,26 @@ "course.achievement.AchievementShow.studentsWithAchievement": { "defaultMessage": "Students with this achievement" }, - "course.achievement.AchievementTable.noAchievement": { - "defaultMessage": "No achievement" + "course.achievement.AchievementTable.actions": { + "defaultMessage": "Actions" }, "course.achievement.AchievementTable.badge": { "defaultMessage": "Badge" }, - "course.achievement.AchievementTable.title": { - "defaultMessage": "Title" - }, "course.achievement.AchievementTable.description": { "defaultMessage": "Description" }, - "course.achievement.AchievementTable.requirements": { - "defaultMessage": "Requirements" + "course.achievement.AchievementTable.noAchievement": { + "defaultMessage": "No achievement" }, "course.achievement.AchievementTable.published": { "defaultMessage": "Published" }, - "course.achievement.AchievementTable.actions": { - "defaultMessage": "Actions" + "course.achievement.AchievementTable.requirements": { + "defaultMessage": "Requirements" + }, + "course.achievement.AchievementTable.title": { + "defaultMessage": "Title" }, "course.achievement.AchievementsIndex.achievements": { "defaultMessage": "Achievements" @@ -515,46 +521,6 @@ "course.admin.AssessmentSettings.toTab": { "defaultMessage": "to {tab}" }, - "course.admin.CodaveriSettings.codaveriModel": { - "defaultMessage": "Model" - }, - "course.admin.CodaveriSettings.codaveriModelDescription": { - "defaultMessage": "The AI model used by Codaveri to generate help conversations with students for programming questions." - }, - "course.admin.CodaveriSettings.codaveriSystemPromptDescription": { - "defaultMessage": - "You may customize the behavior of the Codaveri model by providing instructions here. {br} When assisting students, these instructions will be followed in addition to any you have set on the question itself.{br}To reference question-specific details, you may use the following variables within the prompt, writing them with brackets as shown below:" - }, - "course.admin.CodaveriSettings.codaveriSystemPromptProblemDescriptionLine": { - "defaultMessage": "{problemDescriptionVar} : The full description of the coding problem." - }, - "course.admin.CodaveriSettings.codaveriSystemPromptStudentFilePathsLine": { - "defaultMessage": "{studentFilePathsVar} : A comma-separated list of file paths the student is working on." - }, - "course.admin.CodaveriSettings.codaveriSettings": { - "defaultMessage": "Codaveri settings" - }, - "course.admin.CodaveriSettings.codaveriSettingsSubtitle": { - "defaultMessage": "This is currently an experimental feature. Codaveri provides code evaluation and automated code feedback services for students' codes." - }, - "course.admin.CodaveriSettings.feedbackWorkflow": { - "defaultMessage": "Automatic Post-Submission Comments" - }, - "course.admin.CodaveriSettings.feedbackWorkflowDescription": { - "defaultMessage": "When a submission with programming question is finalised," - }, - "course.admin.CodaveriSettings.feedbackWorkflowNone": { - "defaultMessage": "Generate no feedback" - }, - "course.admin.CodaveriSettings.feedbackWorkflowDraft": { - "defaultMessage": "Generate feedback as a draft requiring approval from staff" - }, - "course.admin.CodaveriSettings.feedbackWorkflowPublish": { - "defaultMessage": "Publish feedback directly to student" - }, - "course.admin.CodaveriSettings.error": { - "defaultMessage": "An error occurred while updating the codaveri setting." - }, "course.admin.CodaveriSettings.Some": { "defaultMessage": "Some" }, @@ -570,32 +536,80 @@ "course.admin.CodaveriSettings.codaveriEngineDescription": { "defaultMessage": "Type of codaveri engine used to generate programming code feedback" }, + "course.admin.CodaveriSettings.codaveriEvaluatorSettings": { + "defaultMessage": "Codaveri Evaluator" + }, + "course.admin.CodaveriSettings.codaveriModel": { + "defaultMessage": "Model" + }, + "course.admin.CodaveriSettings.codaveriModelDescription": { + "defaultMessage": "The AI model used by Codaveri to generate help conversations with students for programming questions." + }, "course.admin.CodaveriSettings.codaveriOverrideSystemPrompt": { "defaultMessage": "Use a custom system prompt" }, "course.admin.CodaveriSettings.codaveriOverrideSystemPromptDescription": { "defaultMessage": "When assisting students, these instructions will be followed in addition to any you have set on the question itself. To reference question-specific details, you may use these variables within the prompt, writing them with brackets as shown below:" }, + "course.admin.CodaveriSettings.codaveriSettings": { + "defaultMessage": "Codaveri settings" + }, + "course.admin.CodaveriSettings.codaveriSettingsSubtitle": { + "defaultMessage": "This is currently an experimental feature. Codaveri provides code evaluation and automated code feedback services for students' codes." + }, "course.admin.CodaveriSettings.codaveriSystemPrompt": { "defaultMessage": "System Prompt" }, + "course.admin.CodaveriSettings.codaveriSystemPromptDescription": { + "defaultMessage": "The Codaveri system prompt controls AI behavior when interacting with students." + }, + "course.admin.CodaveriSettings.codaveriSystemPromptProblemDescriptionLine": { + "defaultMessage": "{problemDescriptionVar} : The full description of the coding problem." + }, + "course.admin.CodaveriSettings.codaveriSystemPromptStudentFilePathsLine": { + "defaultMessage": "{studentFilePathsVar} : A comma-separated list of file paths the student is working on." + }, "course.admin.CodaveriSettings.codaveriUseDefaultSystemPrompt": { "defaultMessage": "Use the default system prompt" }, + "course.admin.CodaveriSettings.enableDisableButton": { + "defaultMessage": "{enabled, select, true {Enable} other {Disable}}" + }, + "course.admin.CodaveriSettings.enableDisableEvaluator": { + "defaultMessage": "{enabled, select, true {Enable } other {Disable }} Codaveri Evaluator for {questionCount} programming questions in {title}?" + }, + "course.admin.CodaveriSettings.enableDisableEvaluatorDescription": { + "defaultMessage": "{questionCount} programming questions in this {type} will use {enabled, select, true {Codaveri } other {Default }} evaluator" + }, + "course.admin.CodaveriSettings.enableDisableLiveFeedback": { + "defaultMessage": "{enabled, select, true {Enable } other {Disable }} Get Help for {questionCount} programming questions in {title}?" + }, + "course.admin.CodaveriSettings.errorOccurredWhenUpdatingCodaveriEvaluatorSettings": { + "defaultMessage": "An error occurred while updating the codaveri evaluator settings." + }, + "course.admin.CodaveriSettings.errorOccurredWhenUpdatingLiveFeedbackSettings": { + "defaultMessage": "An error occurred while updating the Get Help settings." + }, "course.admin.CodaveriSettings.evaluatorUpdateSuccess": { "defaultMessage": "{question} is now using {evaluator} evaluator" }, "course.admin.CodaveriSettings.expandAll": { "defaultMessage": "Expand All Questions" }, - "course.admin.CodaveriSettings.programmingQuestionSettings": { - "defaultMessage": "Programming Question Settings" + "course.admin.CodaveriSettings.feedbackWorkflow": { + "defaultMessage": "Automatic Post-Submission Comments" }, - "course.admin.CodaveriSettings.programmingQuestionSettingsSubtitle": { - "defaultMessage": "Enable/disable Codaveri as evaluator for programming questions in various assessments." + "course.admin.CodaveriSettings.feedbackWorkflowDescription": { + "defaultMessage": "When a submission with programming question is finalised," }, - "course.admin.CodaveriSettings.succesfulUpdateAllEvaluator": { - "defaultMessage": "Successfully updated all questions to use {evaluator} evaluator" + "course.admin.CodaveriSettings.feedbackWorkflowDraft": { + "defaultMessage": "Generate feedback as a draft requiring approval from staff" + }, + "course.admin.CodaveriSettings.feedbackWorkflowNone": { + "defaultMessage": "Generate no feedback" + }, + "course.admin.CodaveriSettings.feedbackWorkflowPublish": { + "defaultMessage": "Publish feedback directly to student" }, "course.admin.CodaveriSettings.getHelpUsageLimit": { "defaultMessage": "Limit Get Help messages per student" @@ -603,35 +617,23 @@ "course.admin.CodaveriSettings.getHelpUsageLimitDescription": { "defaultMessage": "If enabled, students will only be able to send a limited number of messages per question. Students will be able to see this limit and how many messages they have left." }, - "course.admin.CodaveriSettings.maxGetHelpUserMessages": { - "defaultMessage": "Maximum messages per question" - }, - "course.admin.CodaveriSettings.errorOccurredWhenUpdatingCodaveriEvaluatorSettings": { - "defaultMessage": "An error occurred while updating the codaveri evaluator settings." - }, - "course.admin.CodaveriSettings.codaveriEvaluatorSettings": { - "defaultMessage": "Codaveri Evaluator" + "course.admin.CodaveriSettings.liveFeedbackEnabledUpdateSuccess": { + "defaultMessage": "Get Help for {question} is now {liveFeedbackEnabled, select, true {enabled} other {disabled}}" }, "course.admin.CodaveriSettings.liveFeedbackSettings": { "defaultMessage": "Get Help" }, - "course.admin.CodaveriSettings.errorOccurredWhenUpdatingLiveFeedbackSettings": { - "defaultMessage": "An error occurred while updating the Get Help settings." - }, - "course.admin.CodaveriSettings.enableDisableButton": { - "defaultMessage": "{enabled, select, true {Enable} other {Disable}}" - }, - "course.admin.CodaveriSettings.enableDisableEvaluator": { - "defaultMessage": "{enabled, select, true {Enable } other {Disable }} Codaveri Evaluator for {questionCount} programming questions in {title}?" + "course.admin.CodaveriSettings.maxGetHelpUserMessages": { + "defaultMessage": "Maximum messages per question" }, - "course.admin.CodaveriSettings.enableDisableLiveFeedback": { - "defaultMessage": "{enabled, select, true {Enable } other {Disable }} Get Help for {questionCount} programming questions in {title}?" + "course.admin.CodaveriSettings.programmingQuestionSettings": { + "defaultMessage": "Programming Question Settings" }, - "course.admin.CodaveriSettings.enableDisableEvaluatorDescription": { - "defaultMessage": "{questionCount} programming questions in this {type} will use {enabled, select, true {Codaveri } other {Default }} evaluator" + "course.admin.CodaveriSettings.programmingQuestionSettingsSubtitle": { + "defaultMessage": "Enable/disable Codaveri as evaluator for programming questions in various assessments." }, - "course.admin.CodaveriSettings.liveFeedbackEnabledUpdateSuccess": { - "defaultMessage": "Get Help for {question} is now {liveFeedbackEnabled, select, true {enabled} other {disabled}}" + "course.admin.CodaveriSettings.succesfulUpdateAllEvaluator": { + "defaultMessage": "Successfully updated all questions to use {evaluator} evaluator" }, "course.admin.CodaveriSettings.successfulUpdateAllLiveFeedbackEnabled": { "defaultMessage": "Successfully {liveFeedbackEnabled, select, true {enabled} other {disabled}} Get Help for all questions" @@ -648,9 +650,15 @@ "course.admin.ComponentSettings.errorOccurredWhenUpdatingComponents": { "defaultMessage": "An error occurred while updating the component settings." }, + "course.admin.ComponentSettings.settingUpComponent": { + "defaultMessage": "Setting up component for this course" + }, "course.admin.CourseSettings.allowUsersToSendEnrolmentRequests": { "defaultMessage": "Allow users to send enrolment requests" }, + "course.admin.CourseSettings.autoApproveEnrolmentRequests": { + "defaultMessage": "Automatically approve enrolment requests" + }, "course.admin.CourseSettings.clearChanges": { "defaultMessage": "Clear changes" }, @@ -678,6 +686,12 @@ "course.admin.CourseSettings.courseSettings": { "defaultMessage": "Course settings" }, + "course.admin.CourseSettings.courseSuspensionMessage": { + "defaultMessage": "Course suspension message" + }, + "course.admin.CourseSettings.courseSuspensionMessageDescription": { + "defaultMessage": "This message will be shown to users while this course is suspended. Leave blank to show a default message." + }, "course.admin.CourseSettings.daysInAdvance": { "defaultMessage": "Days in advance" }, @@ -765,23 +779,14 @@ "course.admin.CourseSettings.stragglersDescription": { "defaultMessage": "Leave no one behind; subsequent closing reference timings will be pushed back if students complete their assessments late." }, - "course.admin.CourseSettings.suspension": { - "defaultMessage": "Access suspension" - }, "course.admin.CourseSettings.suspendCourse": { "defaultMessage": "Suspend course" }, "course.admin.CourseSettings.suspendCourseDescription": { "defaultMessage": "A suspended course is inaccessible to all students. Instructors can still access the course and all student data will be retained." }, - "course.admin.CourseSettings.unsuspendCourse": { - "defaultMessage": "Unsuspend course" - }, - "course.admin.CourseSettings.courseSuspensionMessage": { - "defaultMessage": "Course suspension message" - }, - "course.admin.CourseSettings.courseSuspensionMessageDescription": { - "defaultMessage": "This message will be shown to users while this course is suspended. Leave blank to show a default message." + "course.admin.CourseSettings.suspendCourseFailure": { + "defaultMessage": "An error occurred while suspending this course." }, "course.admin.CourseSettings.suspendCoursePromptText": { "defaultMessage": "Are you sure you want to suspend this course? All students will not be able to access it until it is unsuspended." @@ -789,20 +794,8 @@ "course.admin.CourseSettings.suspendCourseSuccess": { "defaultMessage": "This course has been suspended." }, - "course.admin.CourseSettings.suspendCourseFailure": { - "defaultMessage": "An error occurred while suspending this course." - }, - "course.admin.CourseSettings.unsuspendCourseSuccess": { - "defaultMessage": "This course has been unsuspended." - }, - "course.admin.CourseSettings.unsuspendCourseFailure": { - "defaultMessage": "An error occurred while unsuspending this course." - }, - "course.admin.CourseSettings.userSuspensionMessage": { - "defaultMessage": "User suspension message" - }, - "course.admin.CourseSettings.userSuspensionMessageDescription": { - "defaultMessage": "This message will be shown to individual users whose access to this course has been suspended. Leave blank to show a default message." + "course.admin.CourseSettings.suspension": { + "defaultMessage": "Access suspension" }, "course.admin.CourseSettings.timeSettings": { "defaultMessage": "Time settings" @@ -813,12 +806,27 @@ "course.admin.CourseSettings.titleRequired": { "defaultMessage": "Course name is required." }, + "course.admin.CourseSettings.unsuspendCourse": { + "defaultMessage": "Unsuspend course" + }, + "course.admin.CourseSettings.unsuspendCourseFailure": { + "defaultMessage": "An error occurred while unsuspending this course." + }, + "course.admin.CourseSettings.unsuspendCourseSuccess": { + "defaultMessage": "This course has been unsuspended." + }, "course.admin.CourseSettings.uploadANewImage": { "defaultMessage": "Choose a new image" }, "course.admin.CourseSettings.uploadingLogo": { "defaultMessage": "Uploading your new logo..." }, + "course.admin.CourseSettings.userSuspensionMessage": { + "defaultMessage": "User suspension message" + }, + "course.admin.CourseSettings.userSuspensionMessageDescription": { + "defaultMessage": "This message will be shown to individual users whose access to this course has been suspended. Leave blank to show a default message." + }, "course.admin.CourseSettingst.confirmDeletePlaceholder": { "defaultMessage": "This is your last chance to go back!" }, @@ -873,6 +881,15 @@ "course.admin.ForumsSettings.markPostAsAnswerSetting": { "defaultMessage": "User who can mark a post as answer" }, + "course.admin.GradebookSettings.gradebookSettings": { + "defaultMessage": "Gradebook settings" + }, + "course.admin.GradebookSettings.weightedViewEnabled": { + "defaultMessage": "Enable weighted grade view" + }, + "course.admin.GradebookSettings.weightedViewEnabledHint": { + "defaultMessage": "Enables a \"By weight\" view in the gradebook where staff can configure per-tab weights and see a weighted Total column." + }, "course.admin.LeaderboardSettings.displayUserCount": { "defaultMessage": "Display user count" }, @@ -925,7 +942,10 @@ "defaultMessage": "Component Item Settings" }, "course.admin.LessonPlanSettings.lessonPlanItemSettings": { - "defaultMessage": "Lesson Plan Item Settings" + "defaultMessage": "Item Settings" + }, + "course.admin.LessonPlanSettings.lessonPlanSettings": { + "defaultMessage": "Lesson Plan Settings" }, "course.admin.LessonPlanSettings.noLessonPlanItems": { "defaultMessage": "There are no lesson plan items to configure for lesson plan display." @@ -942,11 +962,14 @@ "course.admin.MaterialSettings.materialsSettings": { "defaultMessage": "Materials settings" }, + "course.admin.NotificationSettings.component": { + "defaultMessage": "Component" + }, "course.admin.NotificationSettings.description": { "defaultMessage": "Description" }, "course.admin.NotificationSettings.emailSettings": { - "defaultMessage": "Email Settings" + "defaultMessage": "Email settings" }, "course.admin.NotificationSettings.noEmailSettings": { "defaultMessage": "None of the enabled components have email settings." @@ -1059,28 +1082,148 @@ "course.admin.NotificationSettings.updateSuccess": { "defaultMessage": "The email setting \"{setting}\" for {user} users has been {action}." }, - "course.admin.SidebarSettings.errorOccurredWhenUpdatingSidebar": { - "defaultMessage": "An error occurred while updating the sidebar ordering." + "course.admin.RagWiseSettings.ForumKnowledgeBaseSwitch.addFailure": { + "defaultMessage": "{forum} could not be added to knowledge base." }, - "course.admin.SidebarSettings.sidebarSettings": { - "defaultMessage": "Student's sidebar ordering" + "course.admin.RagWiseSettings.ForumKnowledgeBaseSwitch.addSuccess": { + "defaultMessage": "{forum} {n, plural, one {has} other {have}} been added to knowledge base." }, - "course.admin.SidebarSettings.sidebarSettingsSubtitle": { - "defaultMessage": "Drag and drop the sidebar items to rearrange." + "course.admin.RagWiseSettings.ForumKnowledgeBaseSwitch.pendingImport": { + "defaultMessage": "Please wait as your request to import forums into knowledge base is being processed. You may close this window while importing is in progress." }, - "course.admin.SidebarSettings.sidebarSettingsUpdated": { - "defaultMessage": "The new sidebar ordering has been applied. Refresh to see the latest changes." + "course.admin.RagWiseSettings.ForumKnowledgeBaseSwitch.removeFailure": { + "defaultMessage": "{forum} could not be removed from knowledge base." }, - "course.admin.VideosSettings.addATab": { - "defaultMessage": "Add a tab" + "course.admin.RagWiseSettings.ForumKnowledgeBaseSwitch.removeSuccess": { + "defaultMessage": "{forum} {n, plural, one {has} other {have}} been removed from knowledge base." }, - "course.admin.VideosSettings.deleteTabPromptAction": { - "defaultMessage": "Delete {title} tab" + "course.admin.RagWiseSettings.KnowledgeBaseSwitch.addFailure": { + "defaultMessage": "{material} could not be added to knowledge base." }, - "course.admin.VideosSettings.deleteTabPromptMessage": { - "defaultMessage": "Deleting this tab will delete all its associated videos and statistics. This action is irreversible." + "course.admin.RagWiseSettings.KnowledgeBaseSwitch.addSuccess": { + "defaultMessage": "{material} {n, plural, one {has} other {have}} been added to knowledge base." }, - "course.admin.VideosSettings.deleteTabPromptTitle": { + "course.admin.RagWiseSettings.KnowledgeBaseSwitch.pendingAdd": { + "defaultMessage": "Please wait as your request to add materials into knowledge base is being processed. You may close this window while adding is in progress." + }, + "course.admin.RagWiseSettings.KnowledgeBaseSwitch.removeFailure": { + "defaultMessage": "{material} could not be removed from knowledge base." + }, + "course.admin.RagWiseSettings.KnowledgeBaseSwitch.removeSuccess": { + "defaultMessage": "{material} {n, plural, one {has} other {have}} been removed from knowledge base." + }, + "course.admin.RagWiseSettings.expandAll": { + "defaultMessage": "Expand all {object}" + }, + "course.admin.RagWiseSettings.forumSectionSubtitle": { + "defaultMessage": "Manage the inclusion or exclusion of forum data from related courses in the knowledge base, allowing users to control its availability to the LLM for generating responses." + }, + "course.admin.RagWiseSettings.forumSectionTitle": { + "defaultMessage": "No related courses found." + }, + "course.admin.RagWiseSettings.knowledgeBaseStatusSettings": { + "defaultMessage": "Knowledge Base" + }, + "course.admin.RagWiseSettings.materialsSectionSubtitle": { + "defaultMessage": "Add/remove pdf/docx/ipynb/txt files in knowledge base, allowing users to control its availability to the LLM for generating responses." + }, + "course.admin.RagWiseSettings.materialsSectionTitle": { + "defaultMessage": "Materials" + }, + "course.admin.RagWiseSettings.ragWiseSettings": { + "defaultMessage": "RagWise settings" + }, + "course.admin.RagWiseSettings.ragWiseSettingsSubtitle": { + "defaultMessage": "This is currently an experimental feature. RagWise uses Retrieval-Augmented Generation to generate contextually aware responses to student's query on forum." + }, + "course.admin.RagWiseSettings.responseWorkflowAuto": { + "defaultMessage": "Automatically respond" + }, + "course.admin.RagWiseSettings.responseWorkflowDescription": { + "defaultMessage": "When students post a question on forum," + }, + "course.admin.RagWiseSettings.responseWorkflowDraft": { + "defaultMessage": "Always draft" + }, + "course.admin.RagWiseSettings.responseWorkflowDraftDescription": { + "defaultMessage": "Generated response will be drafted." + }, + "course.admin.RagWiseSettings.responseWorkflowHighTrust": { + "defaultMessage": "High trust" + }, + "course.admin.RagWiseSettings.responseWorkflowLowTrust": { + "defaultMessage": "Low trust" + }, + "course.admin.RagWiseSettings.responseWorkflowLowTrustDescription": { + "defaultMessage": "Generated response will be conditionally published with {trust}% trust." + }, + "course.admin.RagWiseSettings.responseWorkflowNoAuto": { + "defaultMessage": "Do not automatically respond" + }, + "course.admin.RagWiseSettings.responseWorkflowPublish": { + "defaultMessage": "Always publish" + }, + "course.admin.RagWiseSettings.responseWorkflowPublishDescription": { + "defaultMessage": "Generated response will be immediately published." + }, + "course.admin.RagWiseSettings.responseWorkflowTitle": { + "defaultMessage": "Automatic Forum Response" + }, + "course.admin.RagWiseSettings.roleplayCharacter": { + "defaultMessage": "Specified Character Prompt" + }, + "course.admin.RagWiseSettings.roleplayCharacterLabel": { + "defaultMessage": "Character prompt (Max 200 Characters)" + }, + "course.admin.RagWiseSettings.roleplayDeadpool": { + "defaultMessage": "You must always impersonate Deadpool character in all your responses." + }, + "course.admin.RagWiseSettings.roleplayDeadpoolLabel": { + "defaultMessage": "Deadpool" + }, + "course.admin.RagWiseSettings.roleplayDescription": { + "defaultMessage": "Customise character prompt to change how LLM response" + }, + "course.admin.RagWiseSettings.roleplayNormal": { + "defaultMessage": "" + }, + "course.admin.RagWiseSettings.roleplayNormalLabel": { + "defaultMessage": "No roleplay" + }, + "course.admin.RagWiseSettings.roleplaySubtitle": { + "defaultMessage": "Character that LLM will roleplay as in responses." + }, + "course.admin.RagWiseSettings.roleplayTitle": { + "defaultMessage": "Response Roleplay" + }, + "course.admin.RagWiseSettings.roleplayYoda": { + "defaultMessage": "You must always impersonate Master Yoda character in all your responses." + }, + "course.admin.RagWiseSettings.roleplayYodaLabel": { + "defaultMessage": "Master Yoda" + }, + "course.admin.SidebarSettings.errorOccurredWhenUpdatingSidebar": { + "defaultMessage": "An error occurred while updating the sidebar ordering." + }, + "course.admin.SidebarSettings.sidebarSettings": { + "defaultMessage": "Student's sidebar ordering" + }, + "course.admin.SidebarSettings.sidebarSettingsSubtitle": { + "defaultMessage": "Drag and drop the sidebar items to rearrange." + }, + "course.admin.SidebarSettings.sidebarSettingsUpdated": { + "defaultMessage": "The new sidebar ordering has been applied. Refresh to see the latest changes." + }, + "course.admin.VideosSettings.addATab": { + "defaultMessage": "Add a tab" + }, + "course.admin.VideosSettings.deleteTabPromptAction": { + "defaultMessage": "Delete {title} tab" + }, + "course.admin.VideosSettings.deleteTabPromptMessage": { + "defaultMessage": "Deleting this tab will delete all its associated videos and statistics. This action is irreversible." + }, + "course.admin.VideosSettings.deleteTabPromptTitle": { "defaultMessage": "Delete {title} tab?" }, "course.admin.VideosSettings.errorOccurredWhenCreatingTab": { @@ -1122,6 +1265,51 @@ "course.admin.courseSettings": { "defaultMessage": "Course Settings" }, + "course.admin.storiesSettings.autoCreateAccounts": { + "defaultMessage": "User accounts and chat rooms on Cikgo will automatically be created if they don't yet exist. Information shared with Cikgo is governed by our Privacy Policy and Cikgo's Privacy Policy." + }, + "course.admin.storiesSettings.integrationHint": { + "defaultMessage": "To integrate your course on Cikgo with this course, enter its integration key here. Here's what's going to happen once this course is integrated with Cikgo." + }, + "course.admin.storiesSettings.integrationSettings": { + "defaultMessage": "Integration settings" + }, + "course.admin.storiesSettings.learnTitle": { + "defaultMessage": "Learn page title" + }, + "course.admin.storiesSettings.leaveEmptyToUseDefaultTitle": { + "defaultMessage": "Leave empty to use the default \"Learn\" title." + }, + "course.admin.storiesSettings.onlyOwnersCanManage": { + "defaultMessage": "Only you, Owners, and Managers can configure the integration of this course with Cikgo." + }, + "course.admin.storiesSettings.pingError": { + "defaultMessage": "There was a problem connecting to Cikgo. You may try again at a later time." + }, + "course.admin.storiesSettings.publishTaskCompletions": { + "defaultMessage": "Student's submission statuses will be reflected in their chat rooms in Cikgo." + }, + "course.admin.storiesSettings.pushKey": { + "defaultMessage": "Integration key" + }, + "course.admin.storiesSettings.pushKeyError": { + "defaultMessage": "This integration key doesn't point to a valid course on Cikgo. Please check your settings on Cikgo and try again." + }, + "course.admin.storiesSettings.pushKeyHint": { + "defaultMessage": "Integration keys aren't strictly secretive, but should be handled in confidence." + }, + "course.admin.storiesSettings.pushKeyPointsToCourse": { + "defaultMessage": "This integration key points to {course} on Cikgo." + }, + "course.admin.storiesSettings.redirects": { + "defaultMessage": "When students access this course's root URL, they'll be redirected to the Learn page. The home page is still accessible from the sidebar." + }, + "course.admin.storiesSettings.storiesSettings": { + "defaultMessage": "Stories settings" + }, + "course.admin.storiesSettings.syncs": { + "defaultMessage": "Published assessments, videos, and surveys in this course will be available in and kept in sync with Cikgo as resources." + }, "course.announcement.AnnouncementsDisplay.searchBarPlaceholder": { "defaultMessage": "Search by title or content" }, @@ -1233,6 +1421,12 @@ "course.assessment.AssessmentForm.blockStudentViewingAfterSubmittedHint": { "defaultMessage": "Students will only be able to view their submissions after their grades have been published." }, + "course.assessment.AssessmentForm.blocksAccessesFromInvalidSUS": { + "defaultMessage": "Block accesses from browsers with invalid UA" + }, + "course.assessment.AssessmentForm.blocksAccessesFromInvalidSUSHint": { + "defaultMessage": "If enabled, examinees using browsers with invalid UA (does not contain the specified SUS below) will be blocked from accessing this assessment. Instructors can override access with the session unlock password. Heartbeats from an overridden browser session will be flagged as valid in the PulseGrid." + }, "course.assessment.AssessmentForm.bonusEndAt": { "defaultMessage": "Bonus ends at" }, @@ -1245,9 +1439,6 @@ "course.assessment.AssessmentForm.delayedGradePublicationHint": { "defaultMessage": "If enabled, gradings will not be immediately shown to students. To publish all gradings, you may click Publish Grades in the Submissions page." }, - "course.assessment.AssessmentForm.canEnableCodaveriInComponents": { - "defaultMessage": "Contact the course manager or owner to enable this feature in Components in the Course Settings." - }, "course.assessment.AssessmentForm.description": { "defaultMessage": "Description" }, @@ -1302,12 +1493,24 @@ "course.assessment.AssessmentForm.hasPersonalTimesHint": { "defaultMessage": "Timings for this item will be automatically adjusted for users based on learning rate." }, + "course.assessment.AssessmentForm.hasTimeLimit": { + "defaultMessage": "Automatically submit when timer ends" + }, + "course.assessment.AssessmentForm.hasTimeLimitHint": { + "defaultMessage": "When enabled, each submission will have its own timer and will automatically be finalised when its timer ends." + }, "course.assessment.AssessmentForm.hasToBeMoreThanMinInterval": { "defaultMessage": "Has to be greater than the minimum value." }, "course.assessment.AssessmentForm.hasToBeMoreThanValueMs": { "defaultMessage": "Has to be at least 3000 ms." }, + "course.assessment.AssessmentForm.hasToBeNumber": { + "defaultMessage": "Has to be valid number." + }, + "course.assessment.AssessmentForm.hasToBePositive": { + "defaultMessage": "Has to be positive." + }, "course.assessment.AssessmentForm.hasToBePositiveInteger": { "defaultMessage": "Has to be a positive integer less than 86,400,000 ms" }, @@ -1320,6 +1523,12 @@ "course.assessment.AssessmentForm.intervalHint": { "defaultMessage": "Controls how frequent heartbeats are sent from the students' browsers. Intervals are randomised between these two ranges." }, + "course.assessment.AssessmentForm.koditsuDisabledInCourse": { + "defaultMessage": "Please contact the Course Administrator to enable Koditsu Exam in Course Settings." + }, + "course.assessment.AssessmentForm.liveFeedback": { + "defaultMessage": "Get Help" + }, "course.assessment.AssessmentForm.maxInterval": { "defaultMessage": "Max interval" }, @@ -1329,9 +1538,18 @@ "course.assessment.AssessmentForm.minInterval": { "defaultMessage": "Min interval" }, + "course.assessment.AssessmentForm.minutes": { + "defaultMessage": "minute(s)" + }, "course.assessment.AssessmentForm.modeSwitchingHint": { "defaultMessage": "You can no longer change the grading mode because there are already submissions for this assessment." }, + "course.assessment.AssessmentForm.needSUSAndSessionUnlockPassword": { + "defaultMessage": "You need to specify a SUS and session unlock password to enable this." + }, + "course.assessment.AssessmentForm.noProgrammingQuestion": { + "defaultMessage": "You need to add at least one programming question that can be supported by Codaveri to allow enabling Get Help for this Assessment" + }, "course.assessment.AssessmentForm.noTestCaseChosenError": { "defaultMessage": "Select at least one type of test case" }, @@ -1356,29 +1574,32 @@ "course.assessment.AssessmentForm.personalisedTimelines": { "defaultMessage": "Personalised timelines" }, + "course.assessment.AssessmentForm.proctorWithKoditsu": { + "defaultMessage": "Proctor Exam using Koditsu" + }, "course.assessment.AssessmentForm.published": { "defaultMessage": "Published" }, "course.assessment.AssessmentForm.publishedHint": { "defaultMessage": "Everyone can see this assessment." }, + "course.assessment.AssessmentForm.questionsIncompatibleWithKoditsu": { + "defaultMessage": "Please make sure that all questions in this assessment is compatible with Koditsu before activating proctoring in Koditsu" + }, "course.assessment.AssessmentForm.secret": { "defaultMessage": "Secret UA Substring (SUS)" }, "course.assessment.AssessmentForm.secretHint": { - "defaultMessage": "If provided, Coursemology can automatically flag a connection as valid in PulseGrid if the examinee's User Agent (UA) contains this secret. Otherwise, connections will be flagged only by heartbeat intervals." + "defaultMessage": "If provided, the PulseGrid automatically checks if the examinee's browser's User Agent (UA) contains this secret, and marks connections that do not as invalid. This string is case-sensitive." }, "course.assessment.AssessmentForm.sessionPassword": { "defaultMessage": "Session unlock password" }, - "course.assessment.AssessmentForm.sessionPasswordHint": { - "defaultMessage": "Ideally, do NOT give this password to students." - }, "course.assessment.AssessmentForm.sessionProtection": { "defaultMessage": "Enable session protection" }, "course.assessment.AssessmentForm.sessionProtectionHint": { - "defaultMessage": "If enabled, students can only access their attempt once. Further access will require the session unlock password." + "defaultMessage": "If enabled, students can only access their attempt once. Further access will require the session unlock password. Ideally, do NOT give this password to students." }, "course.assessment.AssessmentForm.showEvaluation": { "defaultMessage": "Show evaluation test cases" @@ -1392,12 +1613,12 @@ "course.assessment.AssessmentForm.showMcqMrqSolution": { "defaultMessage": "Show MCQ/MRQ solution(s)" }, - "course.assessment.AssessmentForm.showRubricToStudents": { - "defaultMessage": "Show rubric breakdown to students" - }, "course.assessment.AssessmentForm.showPrivate": { "defaultMessage": "Show private test cases" }, + "course.assessment.AssessmentForm.showRubricToStudents": { + "defaultMessage": "Show rubric breakdown to students" + }, "course.assessment.AssessmentForm.singlePage": { "defaultMessage": "Single Page" }, @@ -1422,9 +1643,15 @@ "course.assessment.AssessmentForm.timeBonusExp": { "defaultMessage": "Time Bonus EXP" }, + "course.assessment.AssessmentForm.timeLimit": { + "defaultMessage": "Time Limit" + }, "course.assessment.AssessmentForm.title": { "defaultMessage": "Title" }, + "course.assessment.AssessmentForm.toggleLiveFeedbackDescription": { + "defaultMessage": "Enable Get Help feature for all programming questions" + }, "course.assessment.AssessmentForm.unavailableInAutograded": { "defaultMessage": "Unavailable in autograded assessments." }, @@ -1455,12 +1682,6 @@ "course.assessment.AssessmentForm.visibility": { "defaultMessage": "Visibility" }, - "course.assessment.AssessmentForm.toggleLiveFeedbackDescription": { - "defaultMessage": "{enabled, select, true {Enable} other {Disable}} Get Help feature for all programming questions" - }, - "course.assessment.AssessmentForm.noProgrammingQuestion": { - "defaultMessage": "You need to add at least one programming question that can be supported by Codaveri to allow enabling Get Help for this Assessment" - }, "course.assessment.FileManager.addFiles": { "defaultMessage": "Add Files" }, @@ -1506,9 +1727,24 @@ "course.assessment.edit.update": { "defaultMessage": "Save" }, + "course.assessment.generation.allFieldsLocked": { + "defaultMessage": "All fields are locked, so nothing can be generated." + }, "course.assessment.generation.confirmDeleteConversation": { "defaultMessage": "Are you sure you want to delete \"{title}\" and all its history items? THIS ACTION IS IRREVERSIBLE!" }, + "course.assessment.generation.createMode": { + "defaultMessage": "Create New" + }, + "course.assessment.generation.createModeTooltip": { + "defaultMessage": "Generate fresh questions from scratch" + }, + "course.assessment.generation.enhanceMode": { + "defaultMessage": "Enhance" + }, + "course.assessment.generation.enhanceModeTooltip": { + "defaultMessage": "Build upon your current question" + }, "course.assessment.generation.exportAction": { "defaultMessage": "Export" }, @@ -1518,140 +1754,212 @@ "course.assessment.generation.exportError": { "defaultMessage": "An error occurred in exporting this question: {error}" }, - "course.assessment.generation.lockTooltip": { - "defaultMessage": "Lock to prevent changes to this section" - }, - "course.assessment.generation.newTab": { - "defaultMessage": "New" - }, - "course.assessment.generation.openExportDialog": { - "defaultMessage": "Export" + "course.assessment.generation.generateError": { + "defaultMessage": "An error occurred generating question \"{title}\"." }, - "course.assessment.generation.resetConversation": { - "defaultMessage": "Reset" + "course.assessment.generation.generateMcqPage": { + "defaultMessage": "Generate Multiple Choice Question" }, - "course.assessment.generation.unlockTooltip": { - "defaultMessage": "Unlock to continue editing this section" + "course.assessment.generation.generateMrqPage": { + "defaultMessage": "Generate Multiple Response Question" }, - "course.assessment.generation.mrq.numberOfQuestionsField": { - "defaultMessage": "Number of Questions" + "course.assessment.generation.generateMultipleSuccess": { + "defaultMessage": "Successfully generated {count} questions!" }, - "course.assessment.generation.promptPlaceholder": { - "defaultMessage": "Type something here..." + "course.assessment.generation.generatePage": { + "defaultMessage": "Generate Programming Question" }, "course.assessment.generation.generateQuestion": { "defaultMessage": "Generate" }, - "course.assessment.generation.showInactive": { - "defaultMessage": "Show inactive items" - }, - "course.assessment.generation.mrq.numberOfQuestionsRange": { - "defaultMessage": "Please enter a number from {min} to {max}" + "course.assessment.generation.generateSuccess": { + "defaultMessage": "Generation for \"{title}\" successful." }, - "course.assessment.generation.enhanceMode": { - "defaultMessage": "Enhance" + "course.assessment.generation.languageField": { + "defaultMessage": "Language" }, - "course.assessment.generation.createMode": { - "defaultMessage": "Create New" + "course.assessment.generation.loadingSourceError": { + "defaultMessage": "Unable to load source question data." }, - "course.assessment.generation.enhanceModeTooltip": { - "defaultMessage": "Build upon your current question" + "course.assessment.generation.lockTooltip": { + "defaultMessage": "Lock to prevent changes to this section" }, - "course.assessment.generation.createModeTooltip": { - "defaultMessage": "Generate fresh questions from scratch" + "course.assessment.generation.mrq.exportAction": { + "defaultMessage": "Export" }, "course.assessment.generation.mrq.exportDialogHeader": { "defaultMessage": "Export Questions ({exportCount} selected)" }, - "course.assessment.generation.requireNonEmptyOptionError": { - "defaultMessage": "Question must have at least one non-empty option" + "course.assessment.generation.mrq.numberOfQuestionsField": { + "defaultMessage": "Number of Questions" }, - "course.assessment.generation.untitledQuestion": { - "defaultMessage": "Untitled Question" + "course.assessment.generation.mrq.numberOfQuestionsRange": { + "defaultMessage": "Please enter a number from {min} to {max}" }, - "course.assessment.question.multipleResponses.showOptions": { - "defaultMessage": "Show Options" + "course.assessment.generation.newTab": { + "defaultMessage": "New" }, - "course.assessment.question.multipleResponses.hideOptions": { - "defaultMessage": "Hide Options" + "course.assessment.generation.openExportDialog": { + "defaultMessage": "Export" }, - "course.assessment.question.multipleResponses.noOptions": { - "defaultMessage": "No options" + "course.assessment.generation.promptPlaceholder": { + "defaultMessage": "Type something here..." }, - "course.assessment.question.multipleResponses.title": { - "defaultMessage": "Title" + "course.assessment.generation.requireNonEmptyOptionError": { + "defaultMessage": "Question must have at least one non-empty option" }, - "course.assessment.generation.generateMrqPage": { - "defaultMessage": "Generate Multiple Response Question" + "course.assessment.generation.resetConversation": { + "defaultMessage": "Reset" }, - "course.assessment.generation.generateMcqPage": { - "defaultMessage": "Generate Multiple Choice Question" + "course.assessment.generation.showInactive": { + "defaultMessage": "Show inactive items" }, - "course.assessment.generation.generateMultipleSuccess": { - "defaultMessage": "Successfully generated {count} questions!" + "course.assessment.generation.sourceLanguageNotSupported": { + "defaultMessage": "Source question language not supported by the generation tool." }, - "course.assessment.generation.generateSuccess": { - "defaultMessage": "Generation for {title} successful." + "course.assessment.generation.unlockTooltip": { + "defaultMessage": "Unlock to continue editing this section" }, - "course.assessment.generation.generateError": { - "defaultMessage": "An error occurred generating question {title}." + "course.assessment.generation.untitledQuestion": { + "defaultMessage": "Untitled Question" }, - "course.assessment.generation.loadingSourceError": { - "defaultMessage": "Unable to load source question data." + "course.assessment.liveFeedback.comments": { + "defaultMessage": "Comments" }, - "course.assessment.generation.allFieldsLocked": { - "defaultMessage": "All fields are locked, so nothing can be generated." + "course.assessment.liveFeedback.lineHeader": { + "defaultMessage": "Line {lineNumber}" + }, + "course.assessment.liveFeedback.messageTimingTitle": { + "defaultMessage": "Generated at: {usedAt}" + }, + "course.assessment.liveFeedback.questionTitle": { + "defaultMessage": "Question {index}" + }, + "course.assessment.monitoring.accessGrantedForThisSessionOnly": { + "defaultMessage": "Access will be granted only for this browser session." }, "course.assessment.monitoring.alivePresenceHint": { "defaultMessage": "Last heartbeat was received in time." }, "course.assessment.monitoring.alivePresenceHintSUSMatches": { - "defaultMessage": "Last heartbeat was received in time and the SUS matches." + "defaultMessage": "Last heartbeat was received in time and came from an authorised browser, if browser authorisation is enabled." }, "course.assessment.monitoring.blankField": { "defaultMessage": "(blank)" }, + "course.assessment.monitoring.blocksAccessesFromInvalidSUS": { + "defaultMessage": "Block accesses from unauthorised browsers" + }, + "course.assessment.monitoring.blocksAccessesFromInvalidSUSHint": { + "defaultMessage": "If enabled, examinees using unauthorised browsers can't access this assessment. Instructors can override access with the session unlock password. Heartbeats from overridden browser sessions will always be valid (green) in the PulseGrid." + }, + "course.assessment.monitoring.browserAuthorizationMethod": { + "defaultMessage": "Browser authorisation method" + }, + "course.assessment.monitoring.browserAuthorizationMethodHint": { + "defaultMessage": "Choose how sessions are authorised as valid or invalid. Changes apply to all sessions and heartbeats immediately and updates live in PulseGrid." + }, "course.assessment.monitoring.cannotConnectToLiveMonitoringChannel": { "defaultMessage": "Oops, an error occurred when connecting to the live monitoring channel." }, "course.assessment.monitoring.connected": { "defaultMessage": "Connected" }, - "course.assessment.monitoring.connectedToLiveMonitoringChannel": { - "defaultMessage": "Connected to the live monitoring channel" + "course.assessment.monitoring.connecting": { + "defaultMessage": "Connecting" + }, + "course.assessment.monitoring.deltaFromPreviousHeartbeat": { + "defaultMessage": "{ms} ms from previous heartbeat" }, "course.assessment.monitoring.detailsOfNHeartbeats": { - "defaultMessage": "Details of the last {n} heartbeats" + "defaultMessage": "Last {n} heartbeats" }, "course.assessment.monitoring.disconnected": { "defaultMessage": "Disconnected" }, - "course.assessment.monitoring.disconnectedFromLiveMonitoringChannel": { - "defaultMessage": "Disconnected from the live monitoring channel" + "course.assessment.monitoring.enableBrowserAuthorization": { + "defaultMessage": "Authorise browsers that access this assessment" + }, + "course.assessment.monitoring.enableBrowserAuthorizationHint": { + "defaultMessage": "If enabled, PulseGrid will additionally check if an examinee is accessing this assessment from an authorised browser, based on the authorisation method you choose." + }, + "course.assessment.monitoring.examMonitoring": { + "defaultMessage": "Enable exam monitoring" + }, + "course.assessment.monitoring.examMonitoringHint": { + "defaultMessage": "If enabled, examinees' sessions will be monitored in real time from when they attempt the exam until they finalise it or the first 24 hours since their attempt, whichever is earlier. Instructors can monitor these sessions in PulseGrid." + }, + "course.assessment.monitoring.expiredSession": { + "defaultMessage": "Expired session. It has been at least 24 hours since the submission was made." }, "course.assessment.monitoring.filterByGroup": { "defaultMessage": "Filter by Group" }, + "course.assessment.monitoring.firstReceivedHeartbeat": { + "defaultMessage": "First received heartbeat" + }, "course.assessment.monitoring.generatedAt": { "defaultMessage": "Generated at" }, + "course.assessment.monitoring.intervalHint": { + "defaultMessage": "Controls how frequent heartbeats are sent from the examinees' browsers. Intervals are randomised between these two ranges." + }, + "course.assessment.monitoring.invalidBrowser": { + "defaultMessage": "Invalid browser configuration" + }, + "course.assessment.monitoring.invalidBrowserSubtitle": { + "defaultMessage": "Access to this assessment is not allowed with your current browser and/or its configuration. Contact your instructor for assistance." + }, + "course.assessment.monitoring.invalidHeartbeat": { + "defaultMessage": "Invalid" + }, "course.assessment.monitoring.ipAddress": { "defaultMessage": "IP Address" }, - "course.assessment.monitoring.lastHeartbeat": { - "defaultMessage": "Last heartbeat" - }, "course.assessment.monitoring.latePresenceHint": { "defaultMessage": "Next heartbeat hasn't been received in time, but still within the configured inter-heartbeats interval." }, "course.assessment.monitoring.live": { "defaultMessage": "Live" }, + "course.assessment.monitoring.liveHint": { + "defaultMessage": "This heartbeat was immediately received by the server." + }, + "course.assessment.monitoring.liveness": { + "defaultMessage": "Liveness" + }, + "course.assessment.monitoring.loadAllHeartbeats": { + "defaultMessage": "Load all" + }, + "course.assessment.monitoring.maxInterval": { + "defaultMessage": "Max interval" + }, + "course.assessment.monitoring.milliseconds": { + "defaultMessage": "ms" + }, + "course.assessment.monitoring.minInterval": { + "defaultMessage": "Min interval" + }, "course.assessment.monitoring.missingPresenceHint": { - "defaultMessage": "Next heartbeat hasn't been received in time." + "defaultMessage": "Next heartbeat hasn't been received in time, or the last heartbeat came from an unauthorised browser, if browser authorisation is enabled." + }, + "course.assessment.monitoring.needSUSAndSessionUnlockPassword": { + "defaultMessage": "You must enable browser authorisation and set a session unlock password to enable this." }, "course.assessment.monitoring.noActiveSessions": { - "defaultMessage": "No active sessions." + "defaultMessage": "No active sessions. No attempts have been made." + }, + "course.assessment.monitoring.offset": { + "defaultMessage": "Inter-heartbeat offset" + }, + "course.assessment.monitoring.offsetHint": { + "defaultMessage": "Controls how long PulseGrid should wait after the frequency interval before flagging a session as late." + }, + "course.assessment.monitoring.openSubmissionInNewTab": { + "defaultMessage": "Open submission in new tab" + }, + "course.assessment.monitoring.overrideAccess": { + "defaultMessage": "Override access" }, "course.assessment.monitoring.pulsegrid": { "defaultMessage": "PulseGrid" @@ -1662,9 +1970,42 @@ "course.assessment.monitoring.recentActivitiesHint": { "defaultMessage": "These logs will disappear if you close this tab!" }, + "course.assessment.monitoring.resetZoom": { + "defaultMessage": "Reset zoom" + }, + "course.assessment.monitoring.sebConfigKey": { + "defaultMessage": "Safe Exam Browser (SEB) Config Key" + }, + "course.assessment.monitoring.sebConfigKeyFieldHint": { + "defaultMessage": "Your SEB Config Key, not the Browser Exam Key, is generated from your specific SEB configuration. It stays the same across operating systems and SEB versions. Ensure this field is updated if you change your SEB configuration." + }, + "course.assessment.monitoring.sebConfigKeyFieldLabel": { + "defaultMessage": "SEB Config Key" + }, + "course.assessment.monitoring.sebConfigKeyHint": { + "defaultMessage": "Flags a session as valid if the examinee is using Safe Exam Browser (SEB) with a valid configuration. SEB generates a unique Config Key for a specific configuration. This method requires SEB 3.4 for Windows and SEB 3.0 for iOS and macOS, or later." + }, + "course.assessment.monitoring.sebPayload": { + "defaultMessage": "Safe Exam Browser (SEB) Config Key Hash & URL" + }, + "course.assessment.monitoring.secret": { + "defaultMessage": "Secret UA Substring (SUS)" + }, + "course.assessment.monitoring.secretHint": { + "defaultMessage": "If an examinee's browser's User Agent (UA) contains this case-sensitive secret, PulseGrid will flag that session as valid, and invalid otherwise. If you leave this blank, all sessions will be flagged as valid." + }, + "course.assessment.monitoring.sessionUnlockPassword": { + "defaultMessage": "Session unlock password" + }, "course.assessment.monitoring.stale": { "defaultMessage": "Stale" }, + "course.assessment.monitoring.staleHint": { + "defaultMessage": "This heartbeat wasn't immediately received by the server because the examinee's browser was temporarily unreachable. It was cached in the browser, and sent to the server when the browser was reachable again." + }, + "course.assessment.monitoring.stoppedSession": { + "defaultMessage": "Stopped session. Student may have finalised their submission." + }, "course.assessment.monitoring.summaryCorrectAsAt": { "defaultMessage": "Summary correct as at {time}" }, @@ -1672,7 +2013,10 @@ "defaultMessage": "Type" }, "course.assessment.monitoring.userAgent": { - "defaultMessage": "User Agent" + "defaultMessage": "User Agent (UA)" + }, + "course.assessment.monitoring.userAgentHint": { + "defaultMessage": "Flags a session as valid if the examinee's browser's User Agent (UA) contains a secret substring." }, "course.assessment.monitoring.userHeartbeatContinuedStreaming": { "defaultMessage": "{name}'s heartbeat just continued streaming." @@ -1680,9 +2024,66 @@ "course.assessment.monitoring.userHeartbeatNotReceivedInTime": { "defaultMessage": "{name}'s heartbeat wasn't received in time." }, + "course.assessment.monitoring.validHeartbeat": { + "defaultMessage": "Valid" + }, + "course.assessment.monitoring.zoomPanHint": { + "defaultMessage": "Pinch or scroll to zoom. Drag to pan." + }, "course.assessment.newAssessment": { "defaultMessage": "New Assessment" }, + "course.assessment.plagiarism.actions": { + "defaultMessage": "Actions" + }, + "course.assessment.plagiarism.baseSubmission": { + "defaultMessage": "Base Submission" + }, + "course.assessment.plagiarism.cannotManageSubmission": { + "defaultMessage": "You do not have permission to manage this submission." + }, + "course.assessment.plagiarism.comparedSubmission": { + "defaultMessage": "Compared Submission" + }, + "course.assessment.plagiarism.confirmStartMessage": { + "defaultMessage": "Running a new plagiarism check will remove the previous results." + }, + "course.assessment.plagiarism.confirmStartTitle": { + "defaultMessage": "Confirm Plagiarism Check?" + }, + "course.assessment.plagiarism.downloadPdf": { + "defaultMessage": "Download PDF" + }, + "course.assessment.plagiarism.lastRunTime": { + "defaultMessage": "Last run at: {date}" + }, + "course.assessment.plagiarism.notStarted": { + "defaultMessage": "No plagiarism check has been run" + }, + "course.assessment.plagiarism.plagiarism": { + "defaultMessage": "Plagiarism Results" + }, + "course.assessment.plagiarism.results": { + "defaultMessage": "Plagiarism Results (similarity between submissions)" + }, + "course.assessment.plagiarism.searchByStudentName": { + "defaultMessage": "Search by Student Name" + }, + "course.assessment.plagiarism.showSelfPlagiarism": { + "defaultMessage": "Include self-plagiarism comparisons (same student, different courses)" + }, + "course.assessment.plagiarism.similarityScore": { + "defaultMessage": "Similarity Score" + }, + "course.assessment.plagiarism.start": { + "defaultMessage": "New Plagiarism Check" + }, + "course.assessment.plagiarism.status": { + "defaultMessage": "Plagiarism Check Status" + }, + "course.assessment.plagiarism.viewReport": { + "defaultMessage": "View Report" + }, "course.assessment.question.forumPostResponses.enableTextResponse": { "defaultMessage": "Include a text field for students to provide further inputs" }, @@ -1755,6 +2156,9 @@ "course.assessment.question.multipleResponses.grading": { "defaultMessage": "Grading" }, + "course.assessment.question.multipleResponses.hideOptions": { + "defaultMessage": "Hide Options" + }, "course.assessment.question.multipleResponses.ignoresRandomization": { "defaultMessage": "Ignores randomization" }, @@ -1767,18 +2171,39 @@ "course.assessment.question.multipleResponses.maximumGrade": { "defaultMessage": "Maximum grade" }, + "course.assessment.question.multipleResponses.mustBeLessThanMaxAttachmentSize": { + "defaultMessage": "Must be at most {defaultMax}MB." + }, + "course.assessment.question.multipleResponses.mustBeLessThanMaxAttachments": { + "defaultMessage": "Must be at most {defaultMax}." + }, "course.assessment.question.multipleResponses.mustBeLessThanMaxMaximumGrade": { "defaultMessage": "Must be less than 1000." }, + "course.assessment.question.multipleResponses.mustHaveAtLeastOneResponse": { + "defaultMessage": "You must specify at least one response." + }, "course.assessment.question.multipleResponses.mustSpecifyAtLeastOneCorrectChoice": { "defaultMessage": "You must specify at least one correct choice." }, "course.assessment.question.multipleResponses.mustSpecifyChoice": { "defaultMessage": "You must specify a valid choice title." }, + "course.assessment.question.multipleResponses.mustSpecifyMaxAttachment": { + "defaultMessage": "You must specify a valid, positive maximum attachment number." + }, + "course.assessment.question.multipleResponses.mustSpecifyMaxAttachmentSize": { + "defaultMessage": "You must specify a valid, positive maximum attachment size." + }, "course.assessment.question.multipleResponses.mustSpecifyMaximumGrade": { "defaultMessage": "You must specify a valid, non-negative maximum grade to award." }, + "course.assessment.question.multipleResponses.mustSpecifyPositiveMaxAttachment": { + "defaultMessage": "Max Number of Attachments has to be at least 2." + }, + "course.assessment.question.multipleResponses.mustSpecifyPositiveMaxAttachmentSize": { + "defaultMessage": "Max Size has to be positive." + }, "course.assessment.question.multipleResponses.mustSpecifyPositiveMaximumGrade": { "defaultMessage": "Maximum grade has to be non-negative." }, @@ -1791,6 +2216,9 @@ "course.assessment.question.multipleResponses.newResponseCannotUndo": { "defaultMessage": "This is a new response. It will immediately disappear if you delete before saving it." }, + "course.assessment.question.multipleResponses.noOptions": { + "defaultMessage": "No options" + }, "course.assessment.question.multipleResponses.noSkillsCanCreateSkills": { "defaultMessage": "There are no skills in this course yet. You can create new skills at the Skills page." }, @@ -1827,6 +2255,9 @@ "course.assessment.question.multipleResponses.saveChangesFirstBeforeConvertingMcqMrq": { "defaultMessage": "Please save your changes before attempting to convert this question." }, + "course.assessment.question.multipleResponses.showOptions": { + "defaultMessage": "Show Options" + }, "course.assessment.question.multipleResponses.skills": { "defaultMessage": "Skills" }, @@ -1839,6 +2270,9 @@ "course.assessment.question.multipleResponses.staffOnlyCommentsHint": { "defaultMessage": "Useful for internal notes or documentations. Students will never see this." }, + "course.assessment.question.multipleResponses.title": { + "defaultMessage": "Title" + }, "course.assessment.question.multipleResponses.undoDeleteChoice": { "defaultMessage": "Undo delete choice" }, @@ -1893,6 +2327,9 @@ "course.assessment.question.programming.codaveriEvaluatorHint": { "defaultMessage": "On top of the default evaluation, this evaluator will provide automated code feedback powered by Codaveri when the submission is finalised. They will appear as draft comments for the instructors to review, edit, and publish." }, + "course.assessment.question.programming.codaveriEvaluatorNotSupported": { + "defaultMessage": "{languageName} is not supported by the Codaveri evaluator." + }, "course.assessment.question.programming.codeInserts": { "defaultMessage": "Code inserts" }, @@ -1914,15 +2351,18 @@ "course.assessment.question.programming.defaultEvaluator": { "defaultMessage": "Default" }, - "course.assessment.question.programming.defaultEvaluatorDependencyTitle": { - "defaultMessage": "{name}: Installed Dependencies" - }, "course.assessment.question.programming.defaultEvaluatorDependencyDescription": { "defaultMessage": "Submitted code is run in a containerized environment with the following dependencies installed locally.{br}If your programming question requires a dependency not listed below, contact us and we will consider adding it." }, + "course.assessment.question.programming.defaultEvaluatorDependencyTitle": { + "defaultMessage": "{name}: Installed Dependencies" + }, "course.assessment.question.programming.defaultEvaluatorHint": { "defaultMessage": "No fuss; just run the code according to the evaluation package below and report the test results." }, + "course.assessment.question.programming.defaultEvaluatorNotSupported": { + "defaultMessage": "{languageName} is not supported by the default evaluator." + }, "course.assessment.question.programming.dependencySearchText": { "defaultMessage": "Search dependencies by name" }, @@ -1954,7 +2394,7 @@ "defaultMessage": "Hold tight, evaluating all submissions with the new package..." }, "course.assessment.question.programming.evaluationLimits": { - "defaultMessage": "Evaluationlimits" + "defaultMessage": "Evaluation limits" }, "course.assessment.question.programming.evaluationTestCases": { "defaultMessage": "Evaluation test cases" @@ -1971,6 +2411,9 @@ "course.assessment.question.programming.expected": { "defaultMessage": "Expected" }, + "course.assessment.question.programming.expectedOutput": { + "defaultMessage": "Expected Output" + }, "course.assessment.question.programming.expression": { "defaultMessage": "Expression" }, @@ -2001,6 +2444,9 @@ "course.assessment.question.programming.inlineCode": { "defaultMessage": "Inline code" }, + "course.assessment.question.programming.input": { + "defaultMessage": "Input" + }, "course.assessment.question.programming.javaTestCasesHint": { "defaultMessage": "Expressions will be evaluated in the context of the submitted code. Their return values will be compared against the Expected expectations using the expectEquals(expression, expected) void. Its simplified definition is as follows, where Object has been overloaded for all Java primitives." }, @@ -2016,6 +2462,9 @@ "course.assessment.question.programming.languageAndEvaluation": { "defaultMessage": "Language and evaluation" }, + "course.assessment.question.programming.languageDeprecatedWarning": { + "defaultMessage": "Your selected language is deprecated. Please change it to another language." + }, "course.assessment.question.programming.lastUpdated": { "defaultMessage": "Last updated by {by} on {on}." }, @@ -2025,6 +2474,9 @@ "course.assessment.question.programming.liveFeedbackCustomPromptDescription": { "defaultMessage": "Add instructions to guide the generation of Get Help feedback here. If unsure, just leave this blank." }, + "course.assessment.question.programming.liveFeedbackNotSupported": { + "defaultMessage": "Get Help is not supported for {languageName}." + }, "course.assessment.question.programming.lowestGradingPriority": { "defaultMessage": "Lowest grading priority" }, @@ -2052,24 +2504,24 @@ "course.assessment.question.programming.packageCreationModeHint": { "defaultMessage": "You cannot change this mode once this question is successfully created. Choose wisely!" }, - "course.assessment.question.programming.packageImportSuccess": { - "defaultMessage": "The package was successfully imported." + "course.assessment.question.programming.packageImportEvaluationError": { + "defaultMessage": "An error occurred evaluating your solution against its test cases. Please double-check them and try again." + }, + "course.assessment.question.programming.packageImportEvaluationTimeout": { + "defaultMessage": "No response was received from an evaluator within the required time. This may indicate all our evaluators are busy right now, please try again later." + }, + "course.assessment.question.programming.packageImportGenericError": { + "defaultMessage": "The package could not be imported: {error}" }, "course.assessment.question.programming.packageImportInvalidPackage": { "defaultMessage": "The package could not be imported: the uploaded package does not have a valid structure." }, - "course.assessment.question.programming.packageImportEvaluationTimeout": { - "defaultMessage": "No response was received from an evaluator within the required time. This may indicate all our evaluators are busy right now, please try again later." + "course.assessment.question.programming.packageImportSuccess": { + "defaultMessage": "The package was successfully imported." }, "course.assessment.question.programming.packageImportTimeLimitExceeded": { "defaultMessage": "The solution did not finish evaluating the test cases in the specified time limit." }, - "course.assessment.question.programming.packageImportEvaluationError": { - "defaultMessage": "An error occurred evaluating your solution against its test cases. Please double-check them and try again." - }, - "course.assessment.question.programming.packageImportGenericError": { - "defaultMessage": "The package could not be imported: {error}" - }, "course.assessment.question.programming.packageInfoOnline": { "defaultMessage": "Generated evaluation package" }, @@ -2130,6 +2582,9 @@ "course.assessment.question.programming.standardError": { "defaultMessage": "Standard error" }, + "course.assessment.question.programming.standardInputOutputTestCasesHint": { + "defaultMessage": "Each test case launches a separate {language} console environment and provides input via standard input. The environment will combine the Prepend, student submission, and Append scripts into a single program and run it. The standard output of the program will be compared (as a string) to the expected output of the test case. We recommend handling input parsing and function calls in one of these scripts." + }, "course.assessment.question.programming.standardOutput": { "defaultMessage": "Standard output" }, @@ -2157,20 +2612,194 @@ "course.assessment.question.programming.timeLimit": { "defaultMessage": "Time limit" }, - "course.assessment.question.programming.uploadNewPackage": { - "defaultMessage": "Upload a new package" + "course.assessment.question.programming.timeLimitDetail": { + "defaultMessage": "{timeLimit, plural, one {# minute} other {# minutes}}" + }, + "course.assessment.question.programming.uploadNewPackage": { + "defaultMessage": "Upload a new package" + }, + "course.assessment.question.programming.uploadNewPackageHint": { + "defaultMessage": "All existing submissions will be evaluated against this new package once it is successfully imported." + }, + "course.assessment.question.programming.uploadPackage": { + "defaultMessage": "Manually create/edit offline and upload" + }, + "course.assessment.question.programming.uploadPackageHint": { + "defaultMessage": "Pack the package as a ZIP file, then upload it here. Useful for complex test cases or if you host your course's evaluation packages in some version control system (e.g., Git, Mercurial, etc.)." + }, + "course.assessment.question.programminquestion.questionSavedRedirecting": { + "defaultMessage": "Question saved." + }, + "course.assessment.question.rubricBasedResponses.addNewCategory": { + "defaultMessage": "Add new category" + }, + "course.assessment.question.rubricBasedResponses.addNewLevel": { + "defaultMessage": "Add new grade" + }, + "course.assessment.question.rubricBasedResponses.aiGrading": { + "defaultMessage": "AI Grading" + }, + "course.assessment.question.rubricBasedResponses.aiGradingCustomPrompt": { + "defaultMessage": "Custom Prompt" + }, + "course.assessment.question.rubricBasedResponses.aiGradingCustomPromptDescription": { + "defaultMessage": "Add grading instructions (e.g. question context, model answer, feedback tone). Leave blank if unsure." + }, + "course.assessment.question.rubricBasedResponses.aiGradingModelAnswer": { + "defaultMessage": "Model Answer" + }, + "course.assessment.question.rubricBasedResponses.aiGradingModelAnswerDescription": { + "defaultMessage": "Add an example answer that would get the maximum grades in each rubric category. Leave blank if unsure." + }, + "course.assessment.question.rubricBasedResponses.bonusReservedNames": { + "defaultMessage": "After finalization, a special category named 'Moderation' will be added automatically. It allows graders to award bonus or penalty points at their discretion." + }, + "course.assessment.question.rubricBasedResponses.categoryGrade": { + "defaultMessage": "Grade" + }, + "course.assessment.question.rubricBasedResponses.categoryGradeExplanation": { + "defaultMessage": "Explanation" + }, + "course.assessment.question.rubricBasedResponses.categoryMaximumGrade": { + "defaultMessage": "Max" + }, + "course.assessment.question.rubricBasedResponses.categoryName": { + "defaultMessage": "Category Name" + }, + "course.assessment.question.rubricBasedResponses.enableAiGrading": { + "defaultMessage": "Enable AI to auto-grade submissions" + }, + "course.assessment.question.rubricBasedResponses.enableAiGradingDescription": { + "defaultMessage": "AI will assign rubric scores and draft feedback for you to review and publish." + }, + "course.assessment.question.rubricBasedResponses.rubric": { + "defaultMessage": "Rubric" + }, + "course.assessment.question.rubricBasedResponses.rubricHint": { + "defaultMessage": "Rubric is used to grade the student's submission." + }, + "course.assessment.question.rubricPlayground.addAnswersPromptAction": { + "defaultMessage": "Add" + }, + "course.assessment.question.rubricPlayground.addAnswersTitle": { + "defaultMessage": "Add Sample Answers" + }, + "course.assessment.question.rubricPlayground.addExistingAnswers": { + "defaultMessage": "Add existing answers" + }, + "course.assessment.question.rubricPlayground.addRandomStudentAnswers": { + "defaultMessage": "Add {inputComponent} random student answer(s)" + }, + "course.assessment.question.rubricPlayground.addSampleAnswers": { + "defaultMessage": "Add Sample Answers" + }, + "course.assessment.question.rubricPlayground.answer": { + "defaultMessage": "Answer" + }, + "course.assessment.question.rubricPlayground.apply": { + "defaultMessage": "Apply" + }, + "course.assessment.question.rubricPlayground.applyFailure": { + "defaultMessage": "Failed to apply grading results" + }, + "course.assessment.question.rubricPlayground.applySuccess": { + "defaultMessage": "Grading rubric, prompt, and results successfully applied." + }, + "course.assessment.question.rubricPlayground.applyWillGradeAllAnswers": { + "defaultMessage": "Applying this rubric will assign grades to all student answers, including the ones not yet evaluated on this page." + }, + "course.assessment.question.rubricPlayground.applyingRubricGradingData": { + "defaultMessage": "Applying rubric grading data..." + }, + "course.assessment.question.rubricPlayground.categoryHeading": { + "defaultMessage": "C{index}" + }, + "course.assessment.question.rubricPlayground.compare": { + "defaultMessage": "Compare" + }, + "course.assessment.question.rubricPlayground.comparingRevisions": { + "defaultMessage": "Comparing {count} revisions" + }, + "course.assessment.question.rubricPlayground.confirmAIGradingApplication": { + "defaultMessage": "Confirm AI Grading Application" + }, + "course.assessment.question.rubricPlayground.confirmProceed": { + "defaultMessage": "Are you sure you wish to proceed?" + }, + "course.assessment.question.rubricPlayground.dismiss": { + "defaultMessage": "Dismiss" + }, + "course.assessment.question.rubricPlayground.evaluate": { + "defaultMessage": "Evaluate" + }, + "course.assessment.question.rubricPlayground.evaluateAll": { + "defaultMessage": "Evaluate All ({count})" + }, + "course.assessment.question.rubricPlayground.evaluateRemaining": { + "defaultMessage": "Evaluate Remaining ({count})" + }, + "course.assessment.question.rubricPlayground.evaluating": { + "defaultMessage": "Evaluating" + }, + "course.assessment.question.rubricPlayground.feedback": { + "defaultMessage": "Feedback" + }, + "course.assessment.question.rubricPlayground.gradingCategories": { + "defaultMessage": "Grading Categories" + }, + "course.assessment.question.rubricPlayground.gradingPrompt": { + "defaultMessage": "Grading Prompt" + }, + "course.assessment.question.rubricPlayground.gradingPromptDescription": { + "defaultMessage": "Instructions to guide the AI in grading and giving feedback." + }, + "course.assessment.question.rubricPlayground.modelAnswer": { + "defaultMessage": "Model Answer" + }, + "course.assessment.question.rubricPlayground.modelAnswerDescription": { + "defaultMessage": "An example that scores the maximum for each category." + }, + "course.assessment.question.rubricPlayground.noAnswers": { + "defaultMessage": "No sample answers have been added. Add some to get started." + }, + "course.assessment.question.rubricPlayground.notLatestRevisionWarning": { + "defaultMessage": "You have selected to apply a rubric which is not the latest revision saved on this page." + }, + "course.assessment.question.rubricPlayground.questionGrade": { + "defaultMessage": "Grade" + }, + "course.assessment.question.rubricPlayground.reevaluate": { + "defaultMessage": "Re-evaluate" + }, + "course.assessment.question.rubricPlayground.reevaluateAll": { + "defaultMessage": "Re-evaluate All ({count})" + }, + "course.assessment.question.rubricPlayground.rubricPlayground": { + "defaultMessage": "Rubric Playground" + }, + "course.assessment.question.rubricPlayground.sampleAnswerEvaluations": { + "defaultMessage": "Sample Answer Evaluations" + }, + "course.assessment.question.rubricPlayground.savedRubric": { + "defaultMessage": "Saved Rubric, {date}" + }, + "course.assessment.question.rubricPlayground.searchAnswersPlaceholder": { + "defaultMessage": "Search answers by student name or grade" }, - "course.assessment.question.programming.uploadNewPackageHint": { - "defaultMessage": "All existing submissions will be evaluated against this new package once it is successfully imported." + "course.assessment.question.rubricPlayground.student": { + "defaultMessage": "Student" }, - "course.assessment.question.programming.uploadPackage": { - "defaultMessage": "Manually create/edit offline and upload" + "course.assessment.question.rubricPlayground.totalGrade": { + "defaultMessage": "Total" }, - "course.assessment.question.programming.uploadPackageHint": { - "defaultMessage": "Pack the package as a ZIP file, then upload it here. Useful for complex test cases or if you host your course's evaluation packages in some version control system (e.g., Git, Mercurial, etc.)." + "course.assessment.question.rubricPlayground.viewEditRubric": { + "defaultMessage": "View / Edit Rubric" }, - "course.assessment.question.programminquestion.questionSavedRedirecting": { - "defaultMessage": "Question saved." + "course.assessment.question.rubricPlayground.writeAnswerPlaceholder": { + "defaultMessage": "Write the answer here" + }, + "course.assessment.question.rubricPlayground.writeCustomAnswer": { + "defaultMessage": "Write a custom answer" }, "course.assessment.question.scribing.ScribingQuestionForm.cannotBeBlankValidationError": { "defaultMessage": "Cannot be blank." @@ -2241,8 +2870,14 @@ "course.assessment.question.textResponses.addSolution": { "defaultMessage": "Add a new solution" }, - "course.assessment.question.textResponses.allowFileUpload": { - "defaultMessage": "Allow file upload in the answer" + "course.assessment.question.textResponses.attachmentSettingRequired": { + "defaultMessage": "Attachment Setting should be defined in this question" + }, + "course.assessment.question.textResponses.attachmentSettings": { + "defaultMessage": "Attachment Settings" + }, + "course.assessment.question.textResponses.attachmentSettingsDescription": { + "defaultMessage": "When students are attempting this question," }, "course.assessment.question.textResponses.deleteSolution": { "defaultMessage": "Delete solution" @@ -2268,12 +2903,27 @@ "course.assessment.question.textResponses.grade": { "defaultMessage": "Grade" }, + "course.assessment.question.textResponses.isAttachmentRequired": { + "defaultMessage": "Require file upload for this question" + }, "course.assessment.question.textResponses.keyword": { "defaultMessage": "Keyword" }, "course.assessment.question.textResponses.newSolutionCannotUndo": { "defaultMessage": "This is a new solution. It will immediately disappear if you delete before saving it." }, + "course.assessment.question.textResponses.noAttachment": { + "defaultMessage": "No Attachment" + }, + "course.assessment.question.textResponses.noAttachmentDescription": { + "defaultMessage": "They will not be able to upload any attachment." + }, + "course.assessment.question.textResponses.singleFileAttachment": { + "defaultMessage": "Single Attachment" + }, + "course.assessment.question.textResponses.singleFileAttachmentDescription": { + "defaultMessage": "They can only upload one attachment." + }, "course.assessment.question.textResponses.solution": { "defaultMessage": "Solution" }, @@ -2301,140 +2951,17 @@ "course.assessment.question.textResponses.templateTextDescription": { "defaultMessage": "Text that appears in the answer area when students attempt this question for the first time." }, - "course.assessment.question.rubricPlayground.rubricPlayground": { - "defaultMessage": "Rubric Playground" - }, - "course.assessment.question.rubricPlayground.savedRubric": { - "defaultMessage": "Saved Rubric, {date}" - }, - "course.assessment.question.rubricPlayground.viewEditRubric": { - "defaultMessage": "View / Edit Rubric" - }, - "course.assessment.question.rubricPlayground.evaluate": { - "defaultMessage": "Evaluate" - }, - "course.assessment.question.rubricPlayground.compare": { - "defaultMessage": "Compare" - }, - "course.assessment.question.rubricPlayground.apply": { - "defaultMessage": "Apply" - }, - "course.assessment.question.rubricPlayground.confirmAIGradingApplication": { - "defaultMessage": "Confirm AI Grading Application" - }, - "course.assessment.question.rubricPlayground.applyingRubricGradingData": { - "defaultMessage": "Applying rubric grading data..." - }, - "course.assessment.question.rubricPlayground.applySuccess": { - "defaultMessage": "Grading rubric, prompt, and results successfully applied." - }, - "course.assessment.question.rubricPlayground.applyFailure": { - "defaultMessage": "Failed to apply grading results" - }, - "course.assessment.question.rubricPlayground.notLatestRevisionWarning": { - "defaultMessage": "You have selected to apply a rubric which is not the latest revision saved on this page." - }, - "course.assessment.question.rubricPlayground.applyWillGradeAllAnswers": { - "defaultMessage": "Applying this rubric will assign grades to all student answers, including the ones not yet evaluated on this page." - }, - "course.assessment.question.rubricPlayground.confirmProceed": { - "defaultMessage": "Are you sure you wish to proceed?" - }, - "course.assessment.question.rubricPlayground.sampleAnswerEvaluations": { - "defaultMessage": "Sample Answer Evaluations" - }, - "course.assessment.question.rubricPlayground.addSampleAnswers": { - "defaultMessage": "Add Sample Answers" - }, - "course.assessment.question.rubricPlayground.evaluateAll": { - "defaultMessage": "Evaluate All ({count})" - }, - "course.assessment.question.rubricPlayground.reevaluateAll": { - "defaultMessage": "Re-evaluate All ({count})" - }, - "course.assessment.question.rubricPlayground.evaluateRemaining": { - "defaultMessage": "Evaluate Remaining ({count})" - }, - "course.assessment.question.rubricPlayground.comparingRevisions": { - "defaultMessage": "Comparing {count} revisions" - }, - "course.assessment.question.rubricPlayground.addSampleAnswersTitle": { - "defaultMessage": "Add Sample Answers" - }, - "course.assessment.question.rubricPlayground.add": { - "defaultMessage": "Add" - }, - "course.assessment.question.rubricPlayground.addExistingAnswers": { - "defaultMessage": "Add existing answers" - }, - "course.assessment.question.rubricPlayground.student": { - "defaultMessage": "Student" - }, - "course.assessment.question.rubricPlayground.questionGrade": { - "defaultMessage": "Grade" - }, - "course.assessment.question.rubricPlayground.categoryHeading": { - "defaultMessage": "C{index}" - }, - "course.assessment.question.rubricPlayground.answer": { - "defaultMessage": "Answer" - }, - "course.assessment.question.rubricPlayground.searchAnswersPlaceholder": { - "defaultMessage": "Search answers by student name or grade" - }, - "course.assessment.question.rubricPlayground.addRandomStudentAnswers": { - "defaultMessage": "Add {inputComponent} random student answer(s)" - }, - "course.assessment.question.rubricPlayground.writeCustomAnswer": { - "defaultMessage": "Write a custom answer" - }, - "course.assessment.question.rubricPlayground.writeAnswerPlaceholder": { - "defaultMessage": "Write the answer here" - }, - "course.assessment.question.rubricPlayground.dismiss": { - "defaultMessage": "Dismiss" - }, - "course.assessment.question.rubricPlayground.noAnswers": { - "defaultMessage": "No sample answers have been added. Add some to get started." - }, - "course.assessment.question.rubricPlayground.reevaluate": { - "defaultMessage": "Re-evaluate" - }, - "course.assessment.question.rubricPlayground.totalGrade": { - "defaultMessage": "Total" - }, - "course.assessment.question.rubricPlayground.feedback": { - "defaultMessage": "Feedback" - }, - "course.assessment.question.rubricPlayground.evaluating": { - "defaultMessage": "Evaluating" - }, - "course.assessment.question.rubricPlayground.gradingPrompt": { - "defaultMessage": "Grading Prompt" - }, - "course.assessment.question.rubricPlayground.gradingPromptDescription": { - "defaultMessage": "Instructions to guide the AI in grading and giving feedback." - }, - "course.assessment.question.rubricPlayground.modelAnswer": { - "defaultMessage": "Model Answer" - }, - "course.assessment.question.rubricPlayground.modelAnswerDescription": { - "defaultMessage": "An example that scores the maximum for each category." - }, - "course.assessment.question.rubricPlayground.gradingCategories": { - "defaultMessage": "Grading Categories" + "course.assessment.question.textResponses.textResponseNote": { + "defaultMessage": "Note: If no solutions are provided, the autograder will always award the maximum grade." }, - "course.assessment.question.rubricPlayground.addNewCategory": { - "defaultMessage": "Add New Category" - }, - "course.assessment.question.rubricPlayground.categoryName": { - "defaultMessage": "Category Name" + "course.assessment.question.textResponses.undoDeleteSolution": { + "defaultMessage": "Undo delete solution" }, - "course.assessment.question.rubricPlayground.max": { - "defaultMessage": "Max" + "course.assessment.question.textResponses.validAttachmentSettingValues": { + "defaultMessage": "Attachment Settings should be either no attachment, single file attachment, or multiple file attachment" }, - "course.assessment.question.rubricPlayground.addNewGrade": { - "defaultMessage": "Add New Grade" + "course.assessment.question.textResponses.zeroGrade": { + "defaultMessage": "0.0" }, "course.assessment.session.assessmentNotStarted": { "defaultMessage": "The assessment has not started yet. Please come back after {startDate}." @@ -2460,9 +2987,6 @@ "course.assessment.show.assessmentOnlyAvailableFrom": { "defaultMessage": "This assessment will only be available from" }, - "course.assessment.show.audioResponse": { - "defaultMessage": "Audio Response" - }, "course.assessment.show.baseExp": { "defaultMessage": "Base EXP" }, @@ -2496,6 +3020,9 @@ "course.assessment.show.chooseAssessmentToDuplicateInto": { "defaultMessage": "Choose an assessment to duplicate into" }, + "course.assessment.show.comprehension": { + "defaultMessage": "Comprehension" + }, "course.assessment.show.delete": { "defaultMessage": "Delete" }, @@ -2553,9 +3080,15 @@ "course.assessment.show.errorMovingQuestion": { "defaultMessage": "An error occurred while moving the question." }, + "course.assessment.show.failedSyncingWithKoditsu": { + "defaultMessage": "Not Synced with Koditsu" + }, "course.assessment.show.fileUpload": { "defaultMessage": "File Upload" }, + "course.assessment.show.fileUploadDescription": { + "defaultMessage": "Settings for the number of attachments allowed (none, one, or multiple)" + }, "course.assessment.show.files": { "defaultMessage": "Files" }, @@ -2568,20 +3101,20 @@ "course.assessment.show.forumPostResponse": { "defaultMessage": "Forum Post Response" }, - "course.assessment.show.gradedTestCases": { - "defaultMessage": "Graded test cases" - }, "course.assessment.show.generate": { "defaultMessage": "Generate Questions" }, - "course.assessment.show.generateTooltip": { - "defaultMessage": "Collaborate with Codaveri AI to create questions" + "course.assessment.show.generateFromProgrammingQuestion": { + "defaultMessage": "Generate a similar question with Codaveri AI" }, "course.assessment.show.generateFromQuestion": { "defaultMessage": "Generate a similar question with AI" }, - "course.assessment.show.generateFromProgrammingQuestion": { - "defaultMessage": "Generate a similar question with Codaveri AI" + "course.assessment.show.generateTooltip": { + "defaultMessage": "Collaborate with Codaveri AI to create questions" + }, + "course.assessment.show.gradedTestCases": { + "defaultMessage": "Graded test cases" }, "course.assessment.show.gradingMode": { "defaultMessage": "Grading mode" @@ -2598,6 +3131,9 @@ "course.assessment.show.hideOptions": { "defaultMessage": "Hide options" }, + "course.assessment.show.koditsuMode": { + "defaultMessage": "Koditsu" + }, "course.assessment.show.manageComponents": { "defaultMessage": "Manage Components in Course Settings" }, @@ -2646,6 +3182,9 @@ "course.assessment.show.newQuestion": { "defaultMessage": "New Question" }, + "course.assessment.show.newRubricBasedResponse": { + "defaultMessage": "New Rubric Based Response Question" + }, "course.assessment.show.newScribing": { "defaultMessage": "New Scribing Question" }, @@ -2703,6 +3242,9 @@ "course.assessment.show.requirementsHint": { "defaultMessage": "The following items must be fulfilled to unlock this assessment." }, + "course.assessment.show.rubricBasedResponse": { + "defaultMessage": "Rubric-Based Response" + }, "course.assessment.show.scribing": { "defaultMessage": "Scribing" }, @@ -2712,15 +3254,15 @@ "course.assessment.show.showMcqMrqSolution": { "defaultMessage": "Show MCQ/MRQ solutions" }, - "course.assessment.show.showRubricToStudents": { - "defaultMessage": "Show rubric breakdown to students" - }, "course.assessment.show.showMcqSubmitResult": { "defaultMessage": "Show MCQ submit result" }, "course.assessment.show.showOptions": { "defaultMessage": "Show options" }, + "course.assessment.show.showRubricToStudents": { + "defaultMessage": "Show rubric breakdown to students" + }, "course.assessment.show.sureChangingQuestionType": { "defaultMessage": "Sure you're changing this question type?" }, @@ -2730,6 +3272,12 @@ "course.assessment.show.sureDeletingQuestion": { "defaultMessage": "Sure you're deleting this question?" }, + "course.assessment.show.syncedWithKoditsu": { + "defaultMessage": "Synced with Koditsu" + }, + "course.assessment.show.syncingWithKoditsu": { + "defaultMessage": "Syncing with Koditsu" + }, "course.assessment.show.textResponse": { "defaultMessage": "Text Response" }, @@ -2745,6 +3293,9 @@ "course.assessment.show.unsubmittingAndChangingQuestionType": { "defaultMessage": "Unsubmitting submissions and changing your question type..." }, + "course.assessment.show.voiceResponse": { + "defaultMessage": "Audio Response" + }, "course.assessment.show.whileHoldingToCancelMoving": { "defaultMessage": "while holding to cancel moving." }, @@ -2838,102 +3389,39 @@ "course.assessment.skills.SkillsTable.noBranchSelected": { "defaultMessage": "No Skill Branch has been selected." }, - "course.assessment.skills.SkillsTable.noSkill": { - "defaultMessage": "Sorry, no skill found under this skill branch." - }, - "course.assessment.skills.SkillsTable.skills": { - "defaultMessage": "Skills" - }, - "course.assessment.skills.SkillsTable.uncategorised": { - "defaultMessage": "Uncategorised Skills" - }, - "course.assessment.liveFeedback.questionTitle": { - "defaultMessage": "Question {index}" - }, - "course.assessment.liveFeedback.messageTimingTitle": { - "defaultMessage": "Generated at: {usedAt}" - }, - "course.assessment.liveFeedback.liveFeedbackName": { - "defaultMessage": "Get Help" - }, - "course.assessment.liveFeedback.comments": { - "defaultMessage": "Comments" - }, - "course.assessment.liveFeedback.lineHeader": { - "defaultMessage": "Line {lineNumber}" - }, - "course.assessment.submission.GetHelpChatPage.chatInputText": { - "defaultMessage": "How can we help you?" - }, - "course.assessment.submission.GetHelpChatPage.chatMessagesRemaining": { - "defaultMessage": "{numMessages} / {maxMessages} {numMessages, plural, one {message} other {messages}} remaining" - }, - "course.assessment.submission.GetHelpChatPage.noChatMessagesRemaining": { - "defaultMessage": "You have reached the message limit for this question." - }, - "course.assessment.submission.GetHelpChatPage.codeUpdated": { - "defaultMessage": "Code Updated" - }, - "course.assessment.submission.GetHelpChatPage.ConversationArea.lineNumber": { - "defaultMessage": "Line {lineNumber}" - }, - "course.assessment.submission.GetHelpChatPage.ConversationArea.fileNameAndLineNumber": { - "defaultMessage": "{filename}:{lineNumber}" - }, - "course.assessment.submission.GetHelpChatPage.ConversationArea.threadExpired": { - "defaultMessage": "The chat above has ended. Start a new chat?" - }, - "course.assessment.plagiarism.plagiarism": { - "defaultMessage": "Plagiarism Results" - }, - "course.assessment.plagiarism.status": { - "defaultMessage": "Plagiarism Check Status" - }, - "course.assessment.plagiarism.lastRunTime": { - "defaultMessage": "Last run at: {date}" - }, - "course.assessment.plagiarism.start": { - "defaultMessage": "New Plagiarism Check" - }, - "course.assessment.plagiarism.notStarted": { - "defaultMessage": "No plagiarism check has been run" - }, - "course.assessment.plagiarism.confirmStartTitle": { - "defaultMessage": "Confirm Plagiarism Check?" - }, - "course.assessment.plagiarism.confirmStartMessage": { - "defaultMessage": "Running a new plagiarism check will remove the previous results." - }, - "course.assessment.plagiarism.results": { - "defaultMessage": "Plagiarism Results (similarity between submissions)" + "course.assessment.skills.SkillsTable.noSkill": { + "defaultMessage": "Sorry, no skill found under this skill branch." }, - "course.assessment.plagiarism.baseSubmission": { - "defaultMessage": "Base Submission" + "course.assessment.skills.SkillsTable.skills": { + "defaultMessage": "Skills" }, - "course.assessment.plagiarism.comparedSubmission": { - "defaultMessage": "Compared Submission" + "course.assessment.skills.SkillsTable.uncategorised": { + "defaultMessage": "Uncategorised Skills" }, - "course.assessment.plagiarism.similarityScore": { - "defaultMessage": "Similarity Score" + "course.assessment.statistics.ancestorFail": { + "defaultMessage": "Failed to fetch past iterations of this assessment." }, - "course.assessment.plagiarism.actions": { - "defaultMessage": "Actions" + "course.assessment.statistics.ancestorSelect.current": { + "defaultMessage": "Current" }, - "course.assessment.plagiarism.viewReport": { - "defaultMessage": "View Report" + "course.assessment.statistics.ancestorSelect.fromCourse": { + "defaultMessage": "From {courseTitle}" }, - "course.assessment.plagiarism.downloadPdf": { - "defaultMessage": "Download PDF" + "course.assessment.statistics.ancestorSelect.subtitle": { + "defaultMessage": "Compare against past versions of this assessment:" }, - "course.assessment.plagiarism.searchByStudentName": { - "defaultMessage": "Search by Student Name" + "course.assessment.statistics.ancestorSelect.title": { + "defaultMessage": "Duplication History" }, - "course.assessment.plagiarism.showSelfPlagiarism": { - "defaultMessage": "Include self-plagiarism comparisons (same student, different courses)" + "course.assessment.statistics.ancestorStatisticsFail": { + "defaultMessage": "Failed to fetch ancestor's statistics." }, "course.assessment.statistics.answers": { "defaultMessage": "Answers" }, + "course.assessment.statistics.attemptCount": { + "defaultMessage": "Attempt Count" + }, "course.assessment.statistics.attempts.filename": { "defaultMessage": "Question-level Attempt Statistics for {assessment}" }, @@ -2946,21 +3434,78 @@ "course.assessment.statistics.closePrompt": { "defaultMessage": "Close" }, + "course.assessment.statistics.comments": { + "defaultMessage": "Comments" + }, + "course.assessment.statistics.duplicationHistory": { + "defaultMessage": "Duplication History" + }, + "course.assessment.statistics.email": { + "defaultMessage": "Email" + }, + "course.assessment.statistics.fail": { + "defaultMessage": "Failed to fetch statistics." + }, + "course.assessment.statistics.gradeDisplay": { + "defaultMessage": "Grade: {grade} / {maxGrade}" + }, + "course.assessment.statistics.gradeDistribution": { + "defaultMessage": "Grade Distribution" + }, + "course.assessment.statistics.gradeDistribution.datasetLabel": { + "defaultMessage": "Distribution" + }, + "course.assessment.statistics.gradeDistribution.xAxisLabel": { + "defaultMessage": "Grades" + }, + "course.assessment.statistics.gradeDistribution.yAxisLabel": { + "defaultMessage": "Submissions" + }, "course.assessment.statistics.grader": { "defaultMessage": "Grader" }, + "course.assessment.statistics.gradesPerQuestion": { + "defaultMessage": "Grades Per Question" + }, "course.assessment.statistics.grayCellLegend": { "defaultMessage": "Undecided (question is Non-autogradable)" }, "course.assessment.statistics.group": { "defaultMessage": "Group" }, - "course.assessment.statistics.legendHigherusage": { - "defaultMessage": "Higher Usage" + "course.assessment.statistics.header": { + "defaultMessage": "Statistics for {title}" + }, + "course.assessment.statistics.includePhantom": { + "defaultMessage": "Include Phantom Student" + }, + "course.assessment.statistics.legendHigherLabelGrade": { + "defaultMessage": "Higher Grade" + }, + "course.assessment.statistics.legendHigherLabelGradeDiff": { + "defaultMessage": "More Improvement" }, - "course.assessment.statistics.legendLowerUsage": { + "course.assessment.statistics.legendLowerLabelGrade": { + "defaultMessage": "Lower Grade" + }, + "course.assessment.statistics.legendLowerLabelGradeDiff": { + "defaultMessage": "Less Improvement" + }, + "course.assessment.statistics.legendLowerLabelMessagesSent": { "defaultMessage": "Lower Usage" }, + "course.assessment.statistics.legendLowerLabelWordCount": { + "defaultMessage": "Lower Word Count" + }, + "course.assessment.statistics.legendUpperLabelMessagesSent": { + "defaultMessage": "Higher Usage" + }, + "course.assessment.statistics.legendUpperLabelWordCount": { + "defaultMessage": "Higher Word Count" + }, + "course.assessment.statistics.liveFeedback": { + "defaultMessage": "Get Help" + }, "course.assessment.statistics.liveFeedback.filename": { "defaultMessage": "Question-level Get Help Statistics for {assessment}" }, @@ -2985,78 +3530,30 @@ "course.assessment.statistics.nameGroupsSearchText": { "defaultMessage": "Search by Name or Groups" }, + "course.assessment.statistics.noIncludePhantom": { + "defaultMessage": "*All statistics in this duplicated assessments does not include Phantom Students" + }, "course.assessment.statistics.noSubmission": { "defaultMessage": "No submission yet" }, "course.assessment.statistics.onlyForAutogradableAssessment": { "defaultMessage": "This table is only displayed for Assessment with at least one Autograded Questions" }, + "course.assessment.statistics.pastAnswerTitle": { + "defaultMessage": "Submitted At: {submittedAt}" + }, "course.assessment.statistics.questionDisplayTitle": { "defaultMessage": "Q{index} for {student}" }, "course.assessment.statistics.questionIndex": { "defaultMessage": "Q{index}" }, - "course.assessment.statistics.total": { - "defaultMessage": "Total" - }, - "course.assessment.statistics.workflowState": { - "defaultMessage": "Status" - }, - "course.assessment.statistics.ancestorFail": { - "defaultMessage": "Failed to fetch past iterations of this assessment." - }, - "course.assessment.statistics.ancestorStatisticsFail": { - "defaultMessage": "Failed to fetch ancestor's statistics." - }, - "course.assessment.statistics.fail": { - "defaultMessage": "Failed to fetch statistics." - }, - "course.assessment.statistics.gradeDistribution": { - "defaultMessage": "Grade Distribution" - }, - "course.assessment.statistics.gradeViolin.datasetLabel": { - "defaultMessage": "Distribution" - }, - "course.assessment.statistics.gradeViolin.xAxisLabel": { - "defaultMessage": "Grades" - }, - "course.assessment.statistics.gradeViolin.yAxisLabel": { - "defaultMessage": "Submissions" - }, - "course.assessment.statistics.ancestorSelect.current": { - "defaultMessage": "Current" - }, - "course.assessment.statistics.ancestorSelect.fromCourse": { - "defaultMessage": "From {courseTitle}" - }, - "course.assessment.statistics.ancestorSelect.subtitle": { - "defaultMessage": "Compare against past versions of this assessment:" - }, - "course.assessment.statistics.ancestorSelect.title": { - "defaultMessage": "Duplication History" - }, - "course.assessment.statistics.attemptCount": { - "defaultMessage": "Attempt Count" - }, - "course.assessment.statistics.duplicationHistory": { - "defaultMessage": "Duplication History" - }, - "course.assessment.statistics.gradesPerQuestion": { - "defaultMessage": "Grades Per Question" - }, - "course.assessment.statistics.includePhantom": { - "defaultMessage": "Include Phantom Student" - }, - "course.assessment.statistics.liveFeedback": { - "defaultMessage": "Get Help" - }, - "course.assessment.statistics.header": { - "defaultMessage": "Statistics for {title}" - }, "course.assessment.statistics.statistics": { "defaultMessage": "Statistics" }, + "course.assessment.statistics.submissionPage": { + "defaultMessage": "Go to Answer Page" + }, "course.assessment.statistics.submissionStatuses": { "defaultMessage": "Submission Statuses" }, @@ -3075,9 +3572,18 @@ "course.assessment.statistics.submissionTimeGradeChart.xAxisLabel.withoutDeadline": { "defaultMessage": "Submission Date" }, + "course.assessment.statistics.total": { + "defaultMessage": "Total" + }, + "course.assessment.statistics.workflowState": { + "defaultMessage": "Status" + }, "course.assessment.submission.Annotations.comment": { "defaultMessage": "Add Comment" }, + "course.assessment.submission.Answer.rendererNotImplemented": { + "defaultMessage": "The display for this question type has not been implemented yet." + }, "course.assessment.submission.CodaveriFeedbackStatus.codaveriFeedbackStatus": { "defaultMessage": "Codaveri Feedback Status" }, @@ -3096,14 +3602,77 @@ "course.assessment.submission.EvaluatorErrorPanel.emailSubject": { "defaultMessage": "[Bug Report] Evaluator Error" }, + "course.assessment.submission.FileInput.exactlyOneFileUploadAllowed": { + "defaultMessage": "*You must upload EXACTLY 1 file for this question" + }, + "course.assessment.submission.FileInput.fileName": { + "defaultMessage": "{index}. {name}" + }, + "course.assessment.submission.FileInput.fileTooLargeErrorMessage": { + "defaultMessage": "The following files have size larger than allowed ({maxAttachmentSize} MB)" + }, + "course.assessment.submission.FileInput.fileUploadErrorTitle": { + "defaultMessage": "Error in Uploading Files" + }, + "course.assessment.submission.FileInput.onlyOneFileUploadAllowed": { + "defaultMessage": "*You can only upload AT MOST {maxAttachments} file for this question" + }, + "course.assessment.submission.FileInput.requiredUploadLimitedNumberOfFiles": { + "defaultMessage": "*You can upload AT LEAST 1 and AT MOST {maxAttachments} files for this question" + }, + "course.assessment.submission.FileInput.tooManyFilesErrorMessage": { + "defaultMessage": "You have attempted to upload {numFiles} files, but ONLY {maxAttachmentsAllowed} {maxAttachmentsAllowed, plural, one {file} other {files}} can be uploaded {numAttachments, plural, =0 {} one {since 1 file has been uploaded before} other {since {numAttachments} files has been uploaded before}}" + }, "course.assessment.submission.FileInput.uploadDisabled": { "defaultMessage": "File upload disabled" }, "course.assessment.submission.FileInput.uploadLabel": { "defaultMessage": "Drag and drop or click to upload files" }, + "course.assessment.submission.GetHelpChatPage": { + "defaultMessage": "Get Help" + }, + "course.assessment.submission.GetHelpChatPage.ConversationArea.fileNameAndLineNumber": { + "defaultMessage": "{filename}:{lineNumber}" + }, + "course.assessment.submission.GetHelpChatPage.ConversationArea.lineNumber": { + "defaultMessage": "Line {lineNumber}" + }, + "course.assessment.submission.GetHelpChatPage.ConversationArea.threadExpired": { + "defaultMessage": "The chat above has ended. Start a new chat?" + }, + "course.assessment.submission.GetHelpChatPage.chatInputText": { + "defaultMessage": "How can we help you?" + }, + "course.assessment.submission.GetHelpChatPage.chatMessagesRemaining": { + "defaultMessage": "{numMessages} / {maxMessages} {numMessages, plural, one {message} other {messages}} remaining" + }, + "course.assessment.submission.GetHelpChatPage.codeUpdated": { + "defaultMessage": "Code Updated" + }, + "course.assessment.submission.GetHelpChatPage.endOfConversation": { + "defaultMessage": "View code after conversation" + }, + "course.assessment.submission.GetHelpChatPage.failedSyncingWithCodaveri": { + "defaultMessage": "Unavailable" + }, + "course.assessment.submission.GetHelpChatPage.noChatMessagesRemaining": { + "defaultMessage": "You have reached the message limit for this question." + }, + "course.assessment.submission.GetHelpChatPage.syncedWithCodaveri": { + "defaultMessage": "Ready" + }, + "course.assessment.submission.GetHelpChatPage.syncingWithCodaveri": { + "defaultMessage": "Preparing" + }, + "course.assessment.submission.ImportedFileView.delete": { + "defaultMessage": "Delete" + }, "course.assessment.submission.ImportedFileView.deleteConfirmation": { - "defaultMessage": "Are you sure you want to delete this file?" + "defaultMessage": "Are you sure you want to delete \"{fileName}\"?" + }, + "course.assessment.submission.ImportedFileView.deleteTitle": { + "defaultMessage": "Delete File" }, "course.assessment.submission.ImportedFileView.noFiles": { "defaultMessage": "No files uploaded." @@ -3111,17 +3680,11 @@ "course.assessment.submission.ImportedFileView.uploadedFiles": { "defaultMessage": "Uploaded Files:" }, - "course.assessment.submission.Answer.missingAnswer": { - "defaultMessage": "There is no answer submitted for this question - this might be caused by the addition of this question after the submission is submitted." - }, - "course.assessment.submission.answers.AnswerHeader.noPastAnswers": { - "defaultMessage": "No past answers." - }, - "course.assessment.submission.Answer.rendererNotImplemented": { - "defaultMessage": "The display for this question type has not been implemented yet." + "course.assessment.submission.SubmissionEditIndex.TimeLimitBanner.hoursMinutesSeconds": { + "defaultMessage": "{hrs, plural, one {# hour} other {# hours}} {mins, plural, =0 {} one {# minute} other {# minutes}} {secs, plural, =0 {} one {# second} other {# seconds}}" }, - "course.assessment.submission.SubmissionAnswer.viewPastAnswers": { - "defaultMessage": "Past Answers" + "course.assessment.submission.SubmissionEditIndex.TimeLimitBanner.minutesSeconds": { + "defaultMessage": "{secs, plural, one {# second} other {# seconds}}" }, "course.assessment.submission.SubmissionsIndex.accessLogs": { "defaultMessage": "Access Logs" @@ -3156,6 +3719,9 @@ "course.assessment.submission.SubmissionsIndex.experiencePoints": { "defaultMessage": "EXP Awarded" }, + "course.assessment.submission.SubmissionsIndex.fetchFromKoditsu": { + "defaultMessage": "Fetch Submissions from Koditsu" + }, "course.assessment.submission.SubmissionsIndex.forceSubmit": { "defaultMessage": "Force Submit Remaining" }, @@ -3168,12 +3734,12 @@ "course.assessment.submission.SubmissionsIndex.includePhantoms": { "defaultMessage": "Include phantom users" }, - "lib.translations.myStudents": { - "defaultMessage": "My Students" - }, "course.assessment.submission.SubmissionsIndex.phantom": { "defaultMessage": "Phantom User" }, + "course.assessment.submission.SubmissionsIndex.publishAutoFeedback": { + "defaultMessage": "Publish Automated Programming Feedback ({count})" + }, "course.assessment.submission.SubmissionsIndex.publishGrades": { "defaultMessage": "Publish Grades" }, @@ -3183,21 +3749,6 @@ "course.assessment.submission.SubmissionsIndex.remind": { "defaultMessage": "Send Reminder Emails" }, - "lib.translations.staff": { - "defaultMessage": "Staff" - }, - "lib.translations.students": { - "defaultMessage": "Students" - }, - "lib.translations.myStudentsIncludingPhantoms": { - "defaultMessage": "My Students (Including Phantoms)" - }, - "lib.translations.studentsIncludingPhantoms": { - "defaultMessage": "Students (Including Phantoms)" - }, - "lib.translations.staffIncludingPhantoms": { - "defaultMessage": "Staff (Including Phantoms)" - }, "course.assessment.submission.SubmissionsIndex.submissionStatus": { "defaultMessage": "Status" }, @@ -3213,6 +3764,9 @@ "course.assessment.submission.SubmissionsIndex.userName": { "defaultMessage": "Name" }, + "course.assessment.submission.TestCaseView.allFailed": { + "defaultMessage": "All failed" + }, "course.assessment.submission.TestCaseView.allPassed": { "defaultMessage": "All passed" }, @@ -3252,8 +3806,17 @@ "course.assessment.submission.TestCaseView.standardOutput": { "defaultMessage": "Standard Output" }, + "course.assessment.submission.TestCaseView.testCasesPassed": { + "defaultMessage": "{numPassed}/{numTestCases} passed" + }, "course.assessment.submission.UploadedFileView.deleteConfirmation": { - "defaultMessage": "Are you sure you want to delete this attachment?" + "defaultMessage": "Are you sure you want to delete {fileName}?" + }, + "course.assessment.submission.UploadedFileView.deleteTitle": { + "defaultMessage": "Delete File" + }, + "course.assessment.submission.UploadedFileView.deleting": { + "defaultMessage": "Delete" }, "course.assessment.submission.UploadedFileView.noFiles": { "defaultMessage": "No files uploaded." @@ -3262,7 +3825,7 @@ "defaultMessage": "Uploaded Files" }, "course.assessment.submission.VoiceResponseAnswer.chooseVoiceFileExplain": { - "defaultMessage": "Drag your audio file here, or click to select an audio file. Only wav and mp3 formats are supported. Alternatively, you may use the recorder below to record your response" + "defaultMessage": "Drag and drop or click to upload your WAV / MP3 files. Alternatively, use the recorder below to record your response" }, "course.assessment.submission.VoiceResponseAnswer.pleaseRecordYourVoice": { "defaultMessage": "Please record your voice" @@ -3387,6 +3950,24 @@ "course.assessment.submission.answerSubmitted": { "defaultMessage": "Answer Submitted" }, + "course.assessment.submission.answerTooLarge": { + "defaultMessage": "Answer Too Large" + }, + "course.assessment.submission.answerTooLargeError": { + "defaultMessage": "Your answer must be less than 2 MB." + }, + "course.assessment.submission.answers.AnswerHeader.noPastAnswers": { + "defaultMessage": "No past answers." + }, + "course.assessment.submission.answers.AnswerHeader.viewAllAnswers": { + "defaultMessage": "All Answers ({count})" + }, + "course.assessment.submission.answers.AnswerHeader.viewGetHelpHistory": { + "defaultMessage": "Get Help History ({count})" + }, + "course.assessment.submission.answers.AnswerHeader.viewPastAnswers": { + "defaultMessage": "Past Answers ({count})" + }, "course.assessment.submission.answers.ForumPostResponse.ForumCard.forumCardTitleTypeNoneSelected": { "defaultMessage": "Forum" }, @@ -3459,17 +4040,11 @@ "course.assessment.submission.answers.ForumPostResponse.TopicCard.viewTopicInNewTab": { "defaultMessage": "View Topic" }, - "course.assessment.submission.answers.Programming.ProgrammingFile.downloadFile": { - "defaultMessage": "Download File" - }, "course.assessment.submission.answers.Programming.ProgrammingFile.sizeTooBig": { "defaultMessage": "The file is too big and cannot be displayed." }, - "course.assessment.submission.answerTooLarge": { - "defaultMessage": "Answer Too Large" - }, - "course.assessment.submission.answerTooLargeError": { - "defaultMessage": "Your answer must be less than 2 MB." + "course.assessment.submission.attachmentRequired": { + "defaultMessage": "*please upload AT LEAST 1 file for this question" }, "course.assessment.submission.attemptedAt": { "defaultMessage": "Attempted At" @@ -3492,14 +4067,17 @@ "course.assessment.submission.bonusEndAt": { "defaultMessage": "Bonus End At" }, - "course.assessment.submission.codaveriAutogradeFailure": { - "defaultMessage": "There is an error while evaluating your code in Codaveri. Try submitting your code again in a couple of minutes or check the error message in the network response." + "course.assessment.submission.category": { + "defaultMessage": "Category" }, - "course.assessment.submission.liveFeedbackNoneGenerated": { - "defaultMessage": "Question {questionIndex}: No feedback generated." + "course.assessment.submission.checkAnswer": { + "defaultMessage": "Check Answer" + }, + "course.assessment.submission.checkAnswerWithLimit": { + "defaultMessage": "Check Answer ({attemptsLeft, plural, one {# attempt} other {# attempts}} left)" }, - "course.assessment.submission.liveFeedbackSuccess": { - "defaultMessage": "Question {questionIndex}: Feedback successfully generated." + "course.assessment.submission.codaveriAutogradeFailure": { + "defaultMessage": "There is an error while evaluating your code in Codaveri. Try submitting your code again in a couple of minutes or check the error message in the network response." }, "course.assessment.submission.comment.CodaveriCommentCard.finalise": { "defaultMessage": "Finalise and Post Feedback" @@ -3525,14 +4103,14 @@ "course.assessment.submission.comment.CommentCard.deleteConfirmation": { "defaultMessage": "Are you sure you want to delete this comment?" }, - "course.assessment.submission.comment.CommentCard.save": { - "defaultMessage": "Save" + "course.assessment.submission.comment.CommentCard.isAiGenerated": { + "defaultMessage": "AI Generated Comment" }, "course.assessment.submission.comment.CommentCard.publish": { "defaultMessage": "Publish" }, - "course.assessment.submission.comment.CommentCard.isAiGenerated": { - "defaultMessage": "AI Generated Comment" + "course.assessment.submission.comment.CommentCard.save": { + "defaultMessage": "Save" }, "course.assessment.submission.comment.CommentField.comment": { "defaultMessage": "Comment" @@ -3549,18 +4127,6 @@ "course.assessment.submission.comments": { "defaultMessage": "Comments" }, - "course.assessment.submission.answers.Programming.ProgrammingFiles.liveFeedbackItemDelete": { - "defaultMessage": "Dismiss" - }, - "course.assessment.submission.answers.Programming.ProgrammingFiles.liveFeedbackItemDislike": { - "defaultMessage": "Dislike" - }, - "course.assessment.submission.answers.Programming.ProgrammingFiles.liveFeedbackItemLike": { - "defaultMessage": "Like" - }, - "course.assessment.submission.answers.Programming.ProgrammingFiles.liveFeedbackItemLineHeading": { - "defaultMessage": "Line {linenum}" - }, "course.assessment.submission.continue": { "defaultMessage": "Continue" }, @@ -3606,6 +4172,9 @@ "course.assessment.submission.emptyAssessment": { "defaultMessage": "This assessment currently has no questions." }, + "course.assessment.submission.errorUnknown": { + "defaultMessage": "Error is Unknown" + }, "course.assessment.submission.examDialogMessage": { "defaultMessage": "Please do not sign out or close the browser, otherwise you may have trouble continuing the exam." }, @@ -3615,6 +4184,18 @@ "course.assessment.submission.expAwarded": { "defaultMessage": "EXP Awarded" }, + "course.assessment.submission.explanation": { + "defaultMessage": "Explanation" + }, + "course.assessment.submission.fetchSubmissionsFromKoditsuConfirmation": { + "defaultMessage": "Are you sure you want to fetch all submissions from Koditsu? all the existing answers here will be overwritten by the newer one. NOTE THAT THIS ACTION IS IRREVERSIBLE!" + }, + "course.assessment.submission.fetchSubmissionsFromKoditsuPending": { + "defaultMessage": "Please wait as the submissions are currently being fetched from Koditsu." + }, + "course.assessment.submission.fetchSubmissionsFromKoditsuSuccess": { + "defaultMessage": "All submissions have been fetched successfully from Koditsu" + }, "course.assessment.submission.finalise": { "defaultMessage": "Finalise all answers" }, @@ -3645,6 +4226,9 @@ "course.assessment.submission.grade": { "defaultMessage": "Grade" }, + "course.assessment.submission.gradeDisplay": { + "defaultMessage": "Grade: {grade}" + }, "course.assessment.submission.gradePrefilled": { "defaultMessage": "Pre-filled" }, @@ -3657,9 +4241,6 @@ "course.assessment.submission.gradeSummary": { "defaultMessage": "Grade Summary" }, - "course.assessment.submission.gradeUnsaved": { - "defaultMessage": "Unsaved" - }, "course.assessment.submission.gradeUnsavedHint": { "defaultMessage": "This grade is not yet saved. Click Save Grade at the end of the page to save all grade changes." }, @@ -3675,6 +4256,12 @@ "course.assessment.submission.group": { "defaultMessage": "Group" }, + "course.assessment.submission.history.questionTitle": { + "defaultMessage": "Question Details" + }, + "course.assessment.submission.history.title": { + "defaultMessage": "Submission by {studentName}, Question {number}" + }, "course.assessment.submission.importFilesFailure": { "defaultMessage": "File uploads failed: {errors}" }, @@ -3684,9 +4271,24 @@ "course.assessment.submission.invalidFileUpload": { "defaultMessage": "File uploads failed: Only java files can be uploaded" }, + "course.assessment.submission.isSaved": { + "defaultMessage": "Saved" + }, + "course.assessment.submission.isSaving": { + "defaultMessage": "Saving" + }, + "course.assessment.submission.isUnsaved": { + "defaultMessage": "Unsaved" + }, "course.assessment.submission.lateSubmission": { "defaultMessage": "This submission is LATE! You may want to penalize the student for late submission." }, + "course.assessment.submission.liveFeedbackHistory.codeHistory": { + "defaultMessage": "Code History" + }, + "course.assessment.submission.liveFeedbackNoneGenerated": { + "defaultMessage": "No feedback generated." + }, "course.assessment.submission.loadingComment": { "defaultMessage": "Loading comment field..." }, @@ -3723,6 +4325,9 @@ "course.assessment.submission.mark": { "defaultMessage": "Submit for Publishing" }, + "course.assessment.submission.max": { + "defaultMessage": "Max" + }, "course.assessment.submission.maximumGroupGrade": { "defaultMessage": "Maximum Grade for this Group" }, @@ -3735,6 +4340,9 @@ "course.assessment.submission.ok": { "defaultMessage": "OK" }, + "course.assessment.submission.onlyOneAttachmentAllowed": { + "defaultMessage": "*ONLY 1 file is allowed for this question" + }, "course.assessment.submission.pastAnswers": { "defaultMessage": "Past Answers" }, @@ -3777,14 +4385,20 @@ "course.assessment.submission.question": { "defaultMessage": "Question" }, - "course.assessment.submission.questionNumber": { - "defaultMessage": "Q{number}" + "course.assessment.submission.questionAnswer": { + "defaultMessage": "Answer" }, "course.assessment.submission.questionDescription": { "defaultMessage": "Description" }, - "course.assessment.submission.questionAnswer": { - "defaultMessage": "Answer" + "course.assessment.submission.questionHeading": { + "defaultMessage": "Question {number}" + }, + "course.assessment.submission.questionHeadingWithTitle": { + "defaultMessage": "Question {number}: {title}" + }, + "course.assessment.submission.questionNumber": { + "defaultMessage": "Q{number}" }, "course.assessment.submission.readOnlyEditor.expandComments": { "defaultMessage": "Expand all comments" @@ -3795,6 +4409,12 @@ "course.assessment.submission.reevaluate": { "defaultMessage": "Re-evaluate Answer" }, + "course.assessment.submission.remainingBufferTime": { + "defaultMessage": "Finalising in: {timeLimit}" + }, + "course.assessment.submission.remainingTime": { + "defaultMessage": "Time Remaining: {timeLimit}" + }, "course.assessment.submission.rendererNotImplemented": { "defaultMessage": "The display for this question type has not been implemented yet." }, @@ -3807,14 +4427,8 @@ "course.assessment.submission.resetConfirmation": { "defaultMessage": "Are you sure you want to reset your answer? This action is irreversible and you will lose all your current work for this question." }, - "course.assessment.submission.checkAnswer": { - "defaultMessage": "Check Answer" - }, - "course.assessment.submission.checkAnswerWithLimit": { - "defaultMessage": "Check Answer ({attemptsLeft, plural, one {# attempt} other {# attempts}} left)" - }, - "course.assessment.submission.submitWithLimit": { - "defaultMessage": "Submit ({attemptsLeft, plural, one {# attempt} other {# attempts}} left)" + "course.assessment.submission.rubricScores": { + "defaultMessage": "Rubric Grades" }, "course.assessment.submission.saveDraft": { "defaultMessage": "Save Draft" @@ -3822,6 +4436,15 @@ "course.assessment.submission.saveGrade": { "defaultMessage": "Save Grade" }, + "course.assessment.submission.saved": { + "defaultMessage": "Saved" + }, + "course.assessment.submission.saving": { + "defaultMessage": "Saving" + }, + "course.assessment.submission.savingFailed": { + "defaultMessage": "Saving Failed" + }, "course.assessment.submission.sendReminderEmailConfirmation": { "defaultMessage": "Send reminder emails to {unattempted} unattempted and {attempting} attempting user(s) ({selectedUsers}) who have not completed the assessment?" }, @@ -3858,6 +4481,9 @@ "course.assessment.submission.submissionBy": { "defaultMessage": "Submission by {name}" }, + "course.assessment.submission.submissionError": { + "defaultMessage": "There is a problem in submitting question for {questions}" + }, "course.assessment.submission.submissionsHeader": { "defaultMessage": "Submissions: {assessment}" }, @@ -3870,14 +4496,47 @@ "course.assessment.submission.submitShortcut": { "defaultMessage": "(Ctrl+Enter) or (⌘+Enter)" }, + "course.assessment.submission.submitWithLimit": { + "defaultMessage": "Submit ({attemptsLeft, plural, one {# attempt} other {# attempts}} left)" + }, "course.assessment.submission.submitted": { "defaultMessage": "Submitted" }, "course.assessment.submission.submittedAt": { "defaultMessage": "Submitted At" }, - "course.assessment.submission.unknown": { - "defaultMessage": "Unknown status, please contact administrator" + "course.assessment.submission.suggestions.howDoIFixThis": { + "defaultMessage": "How do I fix this?" + }, + "course.assessment.submission.suggestions.iAmStuck": { + "defaultMessage": "I am stuck" + }, + "course.assessment.submission.suggestions.looksWrong": { + "defaultMessage": "This looks wrong" + }, + "course.assessment.submission.suggestions.optimizeThisCode": { + "defaultMessage": "Review my code" + }, + "course.assessment.submission.suggestions.questionUnclear": { + "defaultMessage": "Explain the question" + }, + "course.assessment.submission.suggestions.whereAmIWrong": { + "defaultMessage": "Where am I wrong?" + }, + "course.assessment.submission.timeIsUp": { + "defaultMessage": "Time is Up!" + }, + "course.assessment.submission.timedAssessmentDialogMessage": { + "defaultMessage": "{stillSomeTimeRemaining, select, true {Once the time is up, the assessment will be automatically finalised.} other {Finalising the submission now!}}" + }, + "course.assessment.submission.timedAssessmentDialogTitle": { + "defaultMessage": "{stillSomeTimeRemaining, select, true {{remainingTime} {isNewSubmission, select, true {} other {remaining}} to complete this assessment.} other {The assessment has ended!}}" + }, + "course.assessment.submission.timedExamDialogMessage": { + "defaultMessage": "{stillSomeTimeRemaining, select, true {Please do not sign out or close the browser while attempting this exam. Once the time is up, the assessment will be automatically finalised.} other {Finalising the submission now!}}" + }, + "course.assessment.submission.timedExamDialogTitle": { + "defaultMessage": "{stillSomeTimeRemaining, select, true {{remainingTime} {isNewSubmission, select, true {} other {remaining}} to complete this exam.} other {The exam has ended!}}" }, "course.assessment.submission.totalGrade": { "defaultMessage": "Total Grade" @@ -3885,6 +4544,9 @@ "course.assessment.submission.type": { "defaultMessage": "Type" }, + "course.assessment.submission.unknown": { + "defaultMessage": "Unknown status, please contact administrator" + }, "course.assessment.submission.unmark": { "defaultMessage": "Revert to Submitted" }, @@ -3898,7 +4560,7 @@ "defaultMessage": "Unsubmit Submission" }, "course.assessment.submission.unsubmitAllConfirmation": { - "defaultMessage": "Are you sure you want to UNSUBMIT the submissions for all {users}? All submissions will be unsubmitted and this will reset the submission time and permit the users to change their answers. NOTE THAT THIS ACTION IS IRREVERSIBLE" + "defaultMessage": "Are you sure you want to UNSUBMIT the submissions for all {users}? All submissions will be unsubmitted and this will reset the submission time and permit the users to change their submissions. NOTE THAT THIS ACTION IS IRREVERSIBLE" }, "course.assessment.submission.unsubmitAllSubmissionsJobPending": { "defaultMessage": "Please wait as the submissions are currently being unsubmitted." @@ -3915,6 +4577,9 @@ "course.assessment.submission.updateFailure": { "defaultMessage": "Submission update failed: {errors}" }, + "course.assessment.submission.updateIndividualSuccess": { + "defaultMessage": "Submission for {errors} updated successfully" + }, "course.assessment.submission.updateSuccess": { "defaultMessage": "Submission updated successfully." }, @@ -3951,9 +4616,6 @@ "course.assessment.submissions.SubmissionsIndex.header": { "defaultMessage": "Submissions" }, - "course.assessment.submission.SubmissionsIndex.publishAutoFeedback": { - "defaultMessage": "Publish Automated Programming Feedback ({count})" - }, "course.assessment.submissions.SubmissionsTable.gradeTooltip": { "defaultMessage": "These grades can't be seen by the student until they are published" }, @@ -3976,7 +4638,7 @@ "defaultMessage": "Submitted At" }, "course.assessment.submissions.SubmissionsTable.tableHeaderTitle": { - "defaultMessage": "Assessment" + "defaultMessage": "Title" }, "course.assessment.submissions.SubmissionsTable.tableHeaderTotalGrade": { "defaultMessage": "Grade" @@ -4035,6 +4697,18 @@ "course.assessments.index.hasTodo": { "defaultMessage": "Has TODO" }, + "course.assessments.index.inviteToKoditsu": { + "defaultMessage": "Invite users to Koditsu Exam" + }, + "course.assessments.index.invitingUserToKoditsu": { + "defaultMessage": "Inviting users to Koditsu Exam" + }, + "course.assessments.index.invitingUserToKoditsuFailure": { + "defaultMessage": "There is a problem in inviting users to Koditsu. Please try again later" + }, + "course.assessments.index.invitingUserToKoditsuSuccess": { + "defaultMessage": "Successful in inviting users to Koditsu Exam" + }, "course.assessments.index.neededFor": { "defaultMessage": "Needed for" }, @@ -4065,6 +4739,9 @@ "course.assessments.index.submittedCount": { "defaultMessage": "Submissions" }, + "course.assessments.index.timeLimitIcon": { + "defaultMessage": "Time Limit: {timeLimit, plural, one {# minute} other {# minutes}}" + }, "course.assessments.index.title": { "defaultMessage": "Title" }, @@ -4083,9 +4760,6 @@ "course.asssessment.submission.submitNoQuestionExplain": { "defaultMessage": "Mark as completed?" }, - "course.admin.NotificationSettings.component": { - "defaultMessage": "Component" - }, "course.componentTitles.course_achievements_component": { "defaultMessage": "Achievements" }, @@ -4170,15 +4844,6 @@ "course.courses.CourseAnnouncements.announcementHeader": { "defaultMessage": "Latest announcements" }, - "course.courses.CourseSuspendedAlert.header": { - "defaultMessage": "This course is suspended. Instructors can still access it, but students cannot." - }, - "course.courses.CourseSuspendedAlert.canSuspendMessage": { - "defaultMessage": "You can unsuspend it from the {link} page." - }, - "course.courses.CourseSuspendedAlert.cannotSuspendMessage": { - "defaultMessage": "If you believe this is a mistake, contact a course manager or owner to have them unsuspend the course." - }, "course.courses.CourseDisplay.noCourse": { "defaultMessage": "There is no course yet..." }, @@ -4224,6 +4889,15 @@ "course.courses.CourseShow.instructorsHeader": { "defaultMessage": "Instructors" }, + "course.courses.CourseSuspendedAlert.canSuspendMessage": { + "defaultMessage": "You can unsuspend it from the {link} page." + }, + "course.courses.CourseSuspendedAlert.cannotSuspendMessage": { + "defaultMessage": "If you believe this is a mistake, contact a course manager or owner to have them unsuspend the course." + }, + "course.courses.CourseSuspendedAlert.header": { + "defaultMessage": "This course is suspended. Instructors can still access it, but students cannot." + }, "course.courses.CourseUserItem.differentCourseNameHint": { "defaultMessage": "You're seeing a name different from your account name because this course's manager invited you with this name." }, @@ -4339,7 +5013,7 @@ "defaultMessage": "Starts at" }, "course.courses.PendingTodosTable.tableHeaderTitle": { - "defaultMessage": "Title" + "defaultMessage": "Assessment" }, "course.courses.PendingTodosTable.tableSeeMore": { "defaultMessage": "See {n} more" @@ -4347,6 +5021,9 @@ "course.courses.Sidebar.administration": { "defaultMessage": "Administration" }, + "course.courses.Sidebar.joinCoursemologyMessage": { + "defaultMessage": "Create a Coursemology account or sign up to join this course." + }, "course.courses.SidebarItem.admin.duplication": { "defaultMessage": "Duplicate Data" }, @@ -4392,15 +5069,15 @@ "course.courses.SidebarItem.home": { "defaultMessage": "Home" }, + "course.courses.SidebarItem.scholaistic.assessments": { + "defaultMessage": "Role-Playing Assessments" + }, "course.courses.SidebarItem.stories.learn": { "defaultMessage": "Learn" }, "course.courses.SidebarItem.stories.missionControl": { "defaultMessage": "Mission Control" }, - "course.courses.SidebarItem.scholaistic.assessments": { - "defaultMessage": "Role-Playing Assessments" - }, "course.courses.TodoIgnoreButton.ignore.ignoreButtonText": { "defaultMessage": "Ignore" }, @@ -4443,6 +5120,12 @@ "course.discussion.topics.CommentCard.deleteSuccess": { "defaultMessage": "Successfully deleted comment." }, + "course.discussion.topics.CommentCard.isAiGenerated": { + "defaultMessage": "AI Generated Comment" + }, + "course.discussion.topics.CommentCard.publish": { + "defaultMessage": "Publish" + }, "course.discussion.topics.CommentCard.publishFailure": { "defaultMessage": "Failed to publish feedback." }, @@ -4464,12 +5147,6 @@ "course.discussion.topics.CommentCard.updateSuccess": { "defaultMessage": "Successfully updated comment." }, - "course.discussion.topics.CommentCard.publish": { - "defaultMessage": "Publish" - }, - "course.discussion.topics.CommentCard.isAiGenerated": { - "defaultMessage": "AI Generated Comment" - }, "course.discussion.topics.CommentField.comment": { "defaultMessage": "Comment" }, @@ -4537,7 +5214,7 @@ "defaultMessage": "Select current instance" }, "course.duplication.Duplication.DestinationCourseSelector.InstanceDropdown.destinationInstance": { - "defaultMessage": "Destination Instance" + "defaultMessage": "Destination instance" }, "course.duplication.Duplication.DestinationCourseSelector.NewCourseForm.newStartAt": { "defaultMessage": "New Start Date *" @@ -4641,15 +5318,15 @@ "course.duplication.Duplication.duplicateData": { "defaultMessage": "Duplicate Data" }, - "course.duplication.Duplication.fromCourse": { - "defaultMessage": "Duplicate Data from {courseTitle}" - }, "course.duplication.Duplication.duplicationDisabled": { "defaultMessage": "Duplication is disabled for this course." }, "course.duplication.Duplication.existingCourse": { "defaultMessage": "Existing Course" }, + "course.duplication.Duplication.fromCourse": { + "defaultMessage": "Duplicate data from {courseTitle}" + }, "course.duplication.Duplication.items": { "defaultMessage": "Selected Items" }, @@ -4743,21 +5420,6 @@ "course.enrolRequests.UserRequests.rejected": { "defaultMessage": "Rejected Enrolment Requests" }, - "course.experiencePoints.downloadCsvButton": { - "defaultMessage": "Download CSV" - }, - "course.experiencePoints.downloadFailure": { - "defaultMessage": "An error occurred while doing your request for download." - }, - "course.experiencePoints.downloadPending": { - "defaultMessage": "Please wait as your request to download is being processed." - }, - "course.experiencePoints.downloadRequestSuccess": { - "defaultMessage": "Your request to download is successful" - }, - "course.experiencePoints.filterByNameButton": { - "defaultMessage": "Filter by Name" - }, "course.experiencePoints.disbursement.DisbursementForm.createDisbursementFailure": { "defaultMessage": "Failed to award experience points." }, @@ -4789,7 +5451,7 @@ "defaultMessage": "Disburse Points" }, "course.experiencePoints.disbursement.DisbursementIndex.disbursements": { - "defaultMessage": "Disbursed Experience Points" + "defaultMessage": "Experience Points" }, "course.experiencePoints.disbursement.DisbursementIndex.experienceTab": { "defaultMessage": "History" @@ -4827,6 +5489,15 @@ "course.experiencePoints.disbursement.FilterForm.weeklyCap": { "defaultMessage": "Weekly Cap" }, + "course.experiencePoints.disbursement.ForumDisbursement.fetchDisbursementFailure": { + "defaultMessage": "Failed to retrieve data." + }, + "course.experiencePoints.disbursement.ForumDisbursement.fetchForumPostsFailure": { + "defaultMessage": "Failed to fetch forum posts." + }, + "course.experiencePoints.disbursement.ForumDisbursement.postListDialogHeader": { + "defaultMessage": "Posts created between {startDate} and {endDate} by" + }, "course.experiencePoints.disbursement.ForumDisbursementForm.createDisbursementFailure": { "defaultMessage": "Failed to award experience points." }, @@ -4836,9 +5507,6 @@ "course.experiencePoints.disbursement.ForumDisbursementForm.fetchForumPostsFailure": { "defaultMessage": "Failed to fetch forum posts." }, - "course.experiencePoints.disbursement.ForumDisbursementForm.postListDialogHeader": { - "defaultMessage": "Posts created between {startDate} and {endDate} by" - }, "course.experiencePoints.disbursement.ForumDisbursementForm.reason": { "defaultMessage": "Reason For Disbursement" }, @@ -4878,6 +5546,27 @@ "course.experiencePoints.disbursement.ForumPostTable.voteTally": { "defaultMessage": "Vote Tally" }, + "course.experiencePoints.disbursement.GeneralDisbursement.fetchDisbursementFailure": { + "defaultMessage": "Failed to retrieve data." + }, + "course.experiencePoints.downloadCsvButton": { + "defaultMessage": "Download CSV" + }, + "course.experiencePoints.downloadFailure": { + "defaultMessage": "An error occurred while doing your request for download." + }, + "course.experiencePoints.downloadPending": { + "defaultMessage": "Please wait as your request to download is being processed." + }, + "course.experiencePoints.downloadRequestSuccess": { + "defaultMessage": "Your request to download is successful" + }, + "course.experiencePoints.fetchRecordsFailure": { + "defaultMessage": "Failed to fetch records" + }, + "course.experiencePoints.filterByNameButton": { + "defaultMessage": "Filter by Name" + }, "course.forum.FormShow.fetchTopicsFailure": { "defaultMessage": "Failed to retrieve forum topic data." }, @@ -5115,48 +5804,57 @@ "course.forum.ForumsIndex.newForum": { "defaultMessage": "New Forum" }, + "course.forum.GenerateReplyButton.generateReply": { + "defaultMessage": "Generate reply" + }, + "course.forum.GenerateReplyButton.generateReplySuccess": { + "defaultMessage": "A reply has been successfully generated." + }, + "course.forum.GenerateReplyButton.generatingReply": { + "defaultMessage": "Generating reply" + }, "course.forum.HideButton.hide": { "defaultMessage": "Hide" }, - "course.forum.HideButton.hideTooltip": { - "defaultMessage": "Hide topic from students" - }, "course.forum.HideButton.hideFailure": { "defaultMessage": "Failed to hide the topic \"{title}\" - {error}" }, "course.forum.HideButton.hideSuccess": { "defaultMessage": "The topic \"{title}\" has successfully been hidden." }, + "course.forum.HideButton.hideTooltip": { + "defaultMessage": "Hide topic from students" + }, "course.forum.HideButton.unhide": { "defaultMessage": "Unhide" }, - "course.forum.HideButton.unhideTooltip": { - "defaultMessage": "Show topic to students" - }, "course.forum.HideButton.unhideFailure": { "defaultMessage": "Failed to unhide the topic \"{title}\" - {error}" }, "course.forum.HideButton.unhideSuccess": { "defaultMessage": "The topic \"{title}\" has successfully been unhidden." }, - "course.forum.LockButton.locked": { - "defaultMessage": "Lock" + "course.forum.HideButton.unhideTooltip": { + "defaultMessage": "Show topic to students" }, "course.forum.LockButton.lockTooltip": { "defaultMessage": "Lock to stop students from posting in this topic" }, + "course.forum.LockButton.locked": { + "defaultMessage": "Lock" + }, "course.forum.LockButton.lockedFailure": { "defaultMessage": "Failed to locked the topic \"{title}\" - {error}" }, "course.forum.LockButton.lockedSuccess": { "defaultMessage": "The topic \"{title}\" has successfully been locked." }, - "course.forum.LockButton.unlocked": { - "defaultMessage": "Unlock" - }, "course.forum.LockButton.unlockTooltip": { "defaultMessage": "Unlock to allow students to post within this topic" }, + "course.forum.LockButton.unlocked": { + "defaultMessage": "Unlock" + }, "course.forum.LockButton.unlockedFailure": { "defaultMessage": "Failed to unlocked the topic \"{title}\" - {error}" }, @@ -5175,6 +5873,12 @@ "course.forum.MarkAnswerButton.markAsAnswer": { "defaultMessage": "Mark as answer" }, + "course.forum.MarkAnswerButton.markAsAnswerAndPublish": { + "defaultMessage": "Mark as answer and publish" + }, + "course.forum.MarkAnswerButton.markAsAnswerAndPublishTooltip": { + "defaultMessage": "Mark as answer and publish for students to view" + }, "course.forum.MarkAnswerButton.markedAsAnswer": { "defaultMessage": "Marked as answer" }, @@ -5274,18 +5978,66 @@ "course.forum.forum.markAllAsReadFailed": { "defaultMessage": "Failed to mark all topics in this forum as read. Please try again later." }, - "course.gradebook.GradebookColumnTree.grades": { - "defaultMessage": "Grades" + "course.forum.publishButton.generateReplyDisabledTooltip": { + "defaultMessage": "Disabled for generated reply" + }, + "course.forum.publishButton.generateReplySuccess": { + "defaultMessage": "Failed to generate a reply." + }, + "course.forum.publishButton.generateReplyTooltip": { + "defaultMessage": "Generate a draft reply using AI" + }, + "course.forum.publishButton.publish": { + "defaultMessage": "Publish" + }, + "course.forum.publishButton.publishFailure": { + "defaultMessage": "Failed to publish the post." + }, + "course.forum.publishButton.publishSuccess": { + "defaultMessage": "The post has succesfully been published." + }, + "course.forum.publishButton.publishTooltip": { + "defaultMessage": "Pusblish post to students" + }, + "course.gradebook.ConfigureWeightsDialog.cancel": { + "defaultMessage": "Cancel" + }, + "course.gradebook.ConfigureWeightsDialog.description": { + "defaultMessage": "Set how much each tab contributes to the total grade. Weights should sum to 100." + }, + "course.gradebook.ConfigureWeightsDialog.dialogTitle": { + "defaultMessage": "Configure tab weights" + }, + "course.gradebook.ConfigureWeightsDialog.save": { + "defaultMessage": "Save" + }, + "course.gradebook.ConfigureWeightsDialog.total": { + "defaultMessage": "Total: {sum}%" + }, + "course.gradebook.ConfigureWeightsDialog.valueTooHigh": { + "defaultMessage": "Value must be at most 100" + }, + "course.gradebook.ConfigureWeightsDialog.valueTooLow": { + "defaultMessage": "Value must be at least 0" + }, + "course.gradebook.ConfigureWeightsDialog.weightsDoNotSum": { + "defaultMessage": "Weights do not sum to 100. Saving is allowed; Total may be inaccurate." + }, + "course.gradebook.GradebookColumnTree.alwaysIncluded": { + "defaultMessage": "Always included" }, "course.gradebook.GradebookColumnTree.email": { "defaultMessage": "Email" }, + "course.gradebook.GradebookColumnTree.gamification": { + "defaultMessage": "Gamification" + }, + "course.gradebook.GradebookColumnTree.grades": { + "defaultMessage": "Grades" + }, "course.gradebook.GradebookColumnTree.level": { "defaultMessage": "Level" }, - "course.gradebook.GradebookColumnTree.alwaysIncluded": { - "defaultMessage": "Always included" - }, "course.gradebook.GradebookColumnTree.name": { "defaultMessage": "Name" }, @@ -5295,8 +6047,14 @@ "course.gradebook.GradebookColumnTree.totalXp": { "defaultMessage": "Total XP" }, - "course.gradebook.GradebookColumnTree.gamification": { - "defaultMessage": "Gamification" + "course.gradebook.GradebookIndex.allAssessments": { + "defaultMessage": "All assessments" + }, + "course.gradebook.GradebookIndex.applyAndExport": { + "defaultMessage": "Apply and Export" + }, + "course.gradebook.GradebookIndex.byWeight": { + "defaultMessage": "By weight" }, "course.gradebook.GradebookIndex.dialogTitle": { "defaultMessage": "Select columns" @@ -5310,33 +6068,63 @@ "course.gradebook.GradebookIndex.exportRows": { "defaultMessage": "Export {count, plural, one {# row} other {# rows}}" }, - "course.gradebook.GradebookIndex.selectColumns": { - "defaultMessage": "Select Columns" - }, - "course.gradebook.GradebookIndex.applyAndExport": { - "defaultMessage": "Apply and Export" - }, "course.gradebook.GradebookIndex.fetchFailure": { "defaultMessage": "Failed to retrieve Gradebook." }, "course.gradebook.GradebookIndex.gradebook": { "defaultMessage": "Gradebook" }, - "course.gradebook.GradebookIndex.searchStudents": { - "defaultMessage": "Search by name or email" - }, "course.gradebook.GradebookIndex.noStudents": { "defaultMessage": "No students enrolled yet" }, "course.gradebook.GradebookIndex.noStudentsHint": { "defaultMessage": "Grades will appear here once students join the course." }, + "course.gradebook.GradebookIndex.searchStudents": { + "defaultMessage": "Search by name or email" + }, + "course.gradebook.GradebookIndex.selectColumns": { + "defaultMessage": "Select Columns" + }, "course.gradebook.GradebookTable.maxMarks": { "defaultMessage": "Max Marks" }, "course.gradebook.GradebookTable.noDataColumnsHint": { "defaultMessage": "No grade columns selected - export will include student info only." }, + "course.gradebook.GradebookTable.noDataColumnsHintWithGamification": { + "defaultMessage": "No grade or gamification columns selected - export will include student info only." + }, + "course.gradebook.GradebookWeightedTable.configureWeights": { + "defaultMessage": "Configure Weights" + }, + "course.gradebook.GradebookWeightedTable.doesNotSumTo100": { + "defaultMessage": "does not sum to 100" + }, + "course.gradebook.GradebookWeightedTable.noWeightsConfigured": { + "defaultMessage": "No weights configured — all tab weights are 0. Use \"Configure Weights\" to assign weights." + }, + "course.gradebook.GradebookWeightedTable.percentOfGrade": { + "defaultMessage": "{weight}% of grade" + }, + "course.gradebook.GradebookWeightedTable.percentTotalExact": { + "defaultMessage": "100% total" + }, + "course.gradebook.GradebookWeightedTable.percentTotalWarning": { + "defaultMessage": "{weight}% total" + }, + "course.gradebook.GradebookWeightedTable.student": { + "defaultMessage": "Student" + }, + "course.gradebook.GradebookWeightedTable.total": { + "defaultMessage": "Total" + }, + "course.gradebook.GradebookWeightedTable.treatUngradedAsZero": { + "defaultMessage": "Treat Ungraded as 0" + }, + "course.gradebook.GradebookWeightedTable.weightsDoNotSum": { + "defaultMessage": "Weights do not sum to 100. Total may be inaccurate." + }, "course.group.GroupCreationForm.description": { "defaultMessage": "Description (Optional)" }, @@ -5613,27 +6401,27 @@ "course.leaderboard.LeaderboardTable.average": { "defaultMessage": "Average" }, - "course.leaderboard.LeaderboardTable.experience": { - "defaultMessage": "Experience" + "course.leaderboard.LeaderboardTable.averageAchievements": { + "defaultMessage": "Average Achievements" }, - "course.leaderboard.LeaderboardTable.rank": { - "defaultMessage": "Rank" + "course.leaderboard.LeaderboardTable.averageExperience": { + "defaultMessage": "Average Experience" }, - "course.leaderboard.LeaderboardTable.name": { - "defaultMessage": "Name" + "course.leaderboard.LeaderboardTable.experience": { + "defaultMessage": "Experience" }, "course.leaderboard.LeaderboardTable.level": { "defaultMessage": "Level" }, - "course.leaderboard.LeaderboardTable.averageExperience": { - "defaultMessage": "Average Experience" - }, - "course.leaderboard.LeaderboardTable.averageAchievements": { - "defaultMessage": "Average Achievements" - }, "course.leaderboard.LeaderboardTable.members": { "defaultMessage": "Members" }, + "course.leaderboard.LeaderboardTable.name": { + "defaultMessage": "Name" + }, + "course.leaderboard.LeaderboardTable.rank": { + "defaultMessage": "Rank" + }, "course.leaderboard.LeaderboardTable.titleAchievements": { "defaultMessage": "By Achievements" }, @@ -5802,20 +6590,56 @@ "course.level.Level.levelHeader": { "defaultMessage": "Levels" }, - "course.level.Level.saveFailure": { - "defaultMessage": "Level saving failed, please try again." + "course.level.Level.orderedIncorrectly": { + "defaultMessage": "Levels will be sorted automatically when saved regardless of their order here." + }, + "course.level.Level.placeholder": { + "defaultMessage": "0" + }, + "course.level.Level.reset": { + "defaultMessage": "Reset" + }, + "course.level.Level.resetTooltip": { + "defaultMessage": "Reset changes" + }, + "course.level.Level.saveChanges": { + "defaultMessage": "Save" }, - "course.level.Level.saveLevels": { - "defaultMessage": "Save Levels" + "course.level.Level.saveFailure": { + "defaultMessage": "Failed to save levels" }, "course.level.Level.saveSuccess": { "defaultMessage": "Levels Saved" }, "course.level.Level.thresholdHeader": { - "defaultMessage": "Threshold" + "defaultMessage": "EXP Threshold" + }, + "course.level.Level.unsavedChanges": { + "defaultMessage": "You have unsaved changes" + }, + "course.material.files.DownloadingFilePage.clickToDownloadFile": { + "defaultMessage": "Download {name}" + }, + "course.material.files.DownloadingFilePage.clickToDownloadFileDescription": { + "defaultMessage": "Something happened when initiating an automatic download. Click the link below to immediately download the file." + }, + "course.material.files.DownloadingFilePage.downloading": { + "defaultMessage": "Downloading {name}" + }, + "course.material.files.DownloadingFilePage.downloadingDescription": { + "defaultMessage": "This file should start downloading automatically now. If it doesn't, you can try again by clicking the link below or refreshing this page." + }, + "course.material.files.DownloadingFilePage.tryDownloadingAgain": { + "defaultMessage": "Try downloading again" + }, + "course.material.files.ErrorRetrievingFilePage.goToTheWorkbin": { + "defaultMessage": "Go to the Workbin" }, - "course.level.LevelRow.zeroThresholdError": { - "defaultMessage": "Experience points threshold cannot be 0" + "course.material.files.ErrorRetrievingFilePage.problemRetrievingFile": { + "defaultMessage": "Problem retrieving file" + }, + "course.material.files.ErrorRetrievingFilePage.problemRetrievingFileDescription": { + "defaultMessage": "Either it no longer exists, you don't have the permission to access it, or something unexpected happened when we were trying to retrieve it." }, "course.material.folders.DownloadFolderButton.downloadFolderErrorMessage": { "defaultMessage": "Download has failed. Please try again later." @@ -5826,6 +6650,15 @@ "course.material.folders.DownloadFolderButton.downloading": { "defaultMessage": "Downloading..." }, + "course.material.folders.ErrorRetrievingFolderPage.goToMainFolder": { + "defaultMessage": "Go to the main folder" + }, + "course.material.folders.ErrorRetrievingFolderPage.problemRetrievingFolder": { + "defaultMessage": "Problem retrieving folder" + }, + "course.material.folders.ErrorRetrievingFolderPage.problemRetrievingFolderDescription": { + "defaultMessage": "Either it no longer exists, you don't have the permission to access it, or something unexpected happened when we were trying to retrieve it." + }, "course.material.folders.FolderEdit.editSubfolderTitle": { "defaultMessage": "Edit Folder" }, @@ -5865,6 +6698,12 @@ "course.material.folders.FolderShow.defaultHeader": { "defaultMessage": "Materials" }, + "course.material.folders.FolderShow.error": { + "defaultMessage": "(Error)" + }, + "course.material.folders.FolderShow.folderNotFound": { + "defaultMessage": "Folder not found" + }, "course.material.folders.MaterialEdit.editMaterialTitle": { "defaultMessage": "Edit Material" }, @@ -5910,41 +6749,71 @@ "course.material.folders.UploadFilesButton.uploadFilesTooltip": { "defaultMessage": "Upload" }, + "course.material.folders.WorkbinTable.lastModified": { + "defaultMessage": "Last Modified" + }, + "course.material.folders.WorkbinTable.name": { + "defaultMessage": "Name" + }, + "course.material.folders.WorkbinTable.startAt": { + "defaultMessage": "Start At" + }, "course.material.folders.WorkbinTableButtons.DeletionFailure": { "defaultMessage": "could not be deleted" }, + "course.material.folders.WorkbinTableButtons.addFailure": { + "defaultMessage": "{material} could not be added to knowledge base" + }, "course.material.folders.WorkbinTableButtons.deleteConfirmation": { "defaultMessage": "Are you sure you want to delete" }, "course.material.folders.WorkbinTableButtons.deletionSuccess": { "defaultMessage": "has been deleted" }, + "course.material.folders.WorkbinTableButtons.removeFailure": { + "defaultMessage": "{material} could not be removed from knowledge base" + }, + "course.material.folders.WorkbinTableButtons.removeSuccess": { + "defaultMessage": "{material} has been removed from knowledge base" + }, "course.material.folders.WorkbinTableButtons.tableButtonDeleteTooltip": { "defaultMessage": "Delete" }, - "course.material.folders.WorkbinTable.name": { - "defaultMessage": "Name" + "course.plagiarism.PlagiarismIndex.assessments.AssessmentLinkDialog.linkAssessments": { + "defaultMessage": "Link Assessments" }, - "course.material.folders.WorkbinTable.lastModified": { - "defaultMessage": "Last Modified" + "course.plagiarism.PlagiarismIndex.assessments.AssessmentLinkDialog.linkedAssessments": { + "defaultMessage": "Linked Assessments" }, - "course.material.folders.WorkbinTable.startAt": { - "defaultMessage": "Start At" + "course.plagiarism.PlagiarismIndex.assessments.AssessmentLinkDialog.searchPlaceholder": { + "defaultMessage": "Search by Assessment Title" }, - "course.plagiarism.PlagiarismIndex.header.plagiarism": { - "defaultMessage": "Plagiarism Check" + "course.plagiarism.PlagiarismIndex.assessments.AssessmentLinkDialog.unlinkedAssessments": { + "defaultMessage": "Available Assessments" }, - "course.plagiarism.PlagiarismIndex.assessments.assessment": { - "defaultMessage": "Assessments" + "course.plagiarism.PlagiarismIndex.assessments.AssessmentLinkDialog.updateLinksFailure": { + "defaultMessage": "Failed to update assessment links" }, - "course.plagiarism.PlagiarismIndex.assessments.numSubmitted": { - "defaultMessage": "# Submissions" + "course.plagiarism.PlagiarismIndex.assessments.AssessmentLinkDialog.updateLinksSuccess": { + "defaultMessage": "Assessment links updated successfully" }, - "course.plagiarism.PlagiarismIndex.assessments.numCheckableQuestions": { - "defaultMessage": "# Checkable Questions" + "course.plagiarism.PlagiarismIndex.assessments.AssessmentLinkList.cannotManage": { + "defaultMessage": "You do not have permission to manage this assessment." }, - "course.plagiarism.PlagiarismIndex.assessments.lastSubmittedAt": { - "defaultMessage": "Last Submission At" + "course.plagiarism.PlagiarismIndex.assessments.AssessmentLinkList.noAssessmentsFound": { + "defaultMessage": "No assessments found" + }, + "course.plagiarism.PlagiarismIndex.assessments.actions": { + "defaultMessage": "Actions" + }, + "course.plagiarism.PlagiarismIndex.assessments.assessment": { + "defaultMessage": "Assessment" + }, + "course.plagiarism.PlagiarismIndex.assessments.confirmRerunMessage": { + "defaultMessage": "Some of the selected assessments already have completed plagiarism checks. Running a new plagiarism check will remove the previous results." + }, + "course.plagiarism.PlagiarismIndex.assessments.confirmRerunTitle": { + "defaultMessage": "Confirm Plagiarism Check?" }, "course.plagiarism.PlagiarismIndex.assessments.lastRunStatus": { "defaultMessage": "Status" @@ -5952,71 +6821,125 @@ "course.plagiarism.PlagiarismIndex.assessments.lastRunTime": { "defaultMessage": "Last Run At" }, - "course.plagiarism.PlagiarismIndex.assessments.statusNotStarted": { - "defaultMessage": "Not Started" + "course.plagiarism.PlagiarismIndex.assessments.lastSubmittedAt": { + "defaultMessage": "Last Submission At" }, - "course.plagiarism.PlagiarismIndex.assessments.statusRunning": { - "defaultMessage": "Running" + "course.plagiarism.PlagiarismIndex.assessments.linkAssessments": { + "defaultMessage": "Link Assessments" }, - "course.plagiarism.PlagiarismIndex.assessments.statusCompleted": { - "defaultMessage": "Completed" + "course.plagiarism.PlagiarismIndex.assessments.newSubmissionsWarning": { + "defaultMessage": "New submissions detected since last plagiarism run" }, - "course.plagiarism.PlagiarismIndex.assessments.statusFailed": { - "defaultMessage": "Failed" + "course.plagiarism.PlagiarismIndex.assessments.noNewSubmissionsWarning": { + "defaultMessage": "No new submissions since last plagiarism run" }, "course.plagiarism.PlagiarismIndex.assessments.noPlagiarismCheckableQuestions": { "defaultMessage": "No checkable questions" }, - "course.plagiarism.PlagiarismIndex.assessments.notEnoughSubmissions": { + "course.plagiarism.PlagiarismIndex.assessments.notEnoughSubmissions": { "defaultMessage": "Not enough submissions" }, + "course.plagiarism.PlagiarismIndex.assessments.numCheckableQuestions": { + "defaultMessage": "# Checkable Questions" + }, + "course.plagiarism.PlagiarismIndex.assessments.numSubmitted": { + "defaultMessage": "# Submissions" + }, "course.plagiarism.PlagiarismIndex.assessments.runAssessmentsPlagiarism": { "defaultMessage": "New Plagiarism Check ({count})" }, - "course.plagiarism.PlagiarismIndex.assessments.runPlagiarismCheckSuccess": { - "defaultMessage": "Started plagiarism check for {count, plural, =1 {# assessment} other {# assessments}}" + "course.plagiarism.PlagiarismIndex.assessments.runPlagiarismCheck": { + "defaultMessage": "Run Plagiarism Check" }, "course.plagiarism.PlagiarismIndex.assessments.runPlagiarismCheckError": { "defaultMessage": "Failed to start plagiarism checks for some assessments" }, + "course.plagiarism.PlagiarismIndex.assessments.runPlagiarismCheckSuccess": { + "defaultMessage": "Started plagiarism check for {count, plural, =1 {# assessment} other {# assessments}}" + }, "course.plagiarism.PlagiarismIndex.assessments.searchByAssessmentTitle": { "defaultMessage": "Search by Assessment Title" }, - "course.plagiarism.PlagiarismIndex.assessments.actions": { - "defaultMessage": "Actions" + "course.plagiarism.PlagiarismIndex.assessments.statusCompleted": { + "defaultMessage": "Completed" }, - "course.plagiarism.PlagiarismIndex.assessments.runPlagiarismCheck": { - "defaultMessage": "Run Plagiarism Check" + "course.plagiarism.PlagiarismIndex.assessments.statusFailed": { + "defaultMessage": "Failed" + }, + "course.plagiarism.PlagiarismIndex.assessments.statusNotStarted": { + "defaultMessage": "Not Started" + }, + "course.plagiarism.PlagiarismIndex.assessments.statusRunning": { + "defaultMessage": "Running" + }, + "course.plagiarism.PlagiarismIndex.assessments.statusStarting": { + "defaultMessage": "Starting" }, "course.plagiarism.PlagiarismIndex.assessments.viewResults": { "defaultMessage": "View Results" }, - "course.plagiarism.PlagiarismIndex.assessments.newSubmissionsWarning": { - "defaultMessage": "New submissions detected since last plagiarism run" + "course.plagiarism.PlagiarismIndex.header.plagiarism": { + "defaultMessage": "Plagiarism Check" }, - "course.plagiarism.PlagiarismIndex.assessments.noNewSubmissionsWarning": { - "defaultMessage": "No new submissions since last plagiarism run" + "course.statistics.StatisticsIndex.assessments.averageGrade": { + "defaultMessage": "Avg Grade" }, - "course.plagiarism.PlagiarismIndex.assessments.confirmRerunTitle": { - "defaultMessage": "Confirm Plagiarism Check?" + "course.statistics.StatisticsIndex.assessments.averageTimeTaken": { + "defaultMessage": "Avg Time" }, - "course.plagiarism.PlagiarismIndex.assessments.confirmRerunMessage": { - "defaultMessage": "Some of the selected assessments already have completed plagiarism checks. Running a new plagiarism check will remove the previous results." + "course.statistics.StatisticsIndex.assessments.category": { + "defaultMessage": "Category" }, - "course.statistics.StatisticsIndex.course.StudentPerformanceTable.achievementCount": { - "defaultMessage": "No. of Achievements (Total: {courseAchievementCount})" + "course.statistics.StatisticsIndex.assessments.csvFileTitle": { + "defaultMessage": "Assessments Statistics" }, - "course.statistics.StatisticsIndex.course.StudentPerformanceTable.ascending": { - "defaultMessage": "Ascending" + "course.statistics.StatisticsIndex.assessments.downloadCsv": { + "defaultMessage": "Download Score Summary for the following Assessments?" }, - "course.statistics.StatisticsIndex.course.StudentPerformanceTable.correctness": { - "defaultMessage": "Correctness" + "course.statistics.StatisticsIndex.assessments.downloadScoreSummaryFailure": { + "defaultMessage": "An error occurred while downloading score summary" + }, + "course.statistics.StatisticsIndex.assessments.downloadScoreSummaryPending": { + "defaultMessage": "Please wait as your request to download is being processed" + }, + "course.statistics.StatisticsIndex.assessments.downloadScoreSummarySuccess": { + "defaultMessage": "Successfully downloaded score summary" + }, + "course.statistics.StatisticsIndex.assessments.numLateStudents": { + "defaultMessage": "# Late" + }, + "course.statistics.StatisticsIndex.assessments.numSubmittedStudents": { + "defaultMessage": "# Attempted" + }, + "course.statistics.StatisticsIndex.assessments.searchBar": { + "defaultMessage": "Search by Assessment Title, Tab, or Category" + }, + "course.statistics.StatisticsIndex.assessments.selectedNUsers": { + "defaultMessage": "Download Score Summary ({n, plural, =1 {# assessment} other {# assessments}})" + }, + "course.statistics.StatisticsIndex.assessments.startAt": { + "defaultMessage": "Starts At" + }, + "course.statistics.StatisticsIndex.assessments.stdevGrade": { + "defaultMessage": "Stdev Grade" }, - "course.statistics.StatisticsIndex.course.StudentPerformanceTable.correctnessHint": { - "defaultMessage": "Correctness is the average grade percentage of all graded assessments by a student." + "course.statistics.StatisticsIndex.assessments.stdevTimeTaken": { + "defaultMessage": "Stdev Time" + }, + "course.statistics.StatisticsIndex.assessments.tab": { + "defaultMessage": "Tab" + }, + "course.statistics.StatisticsIndex.assessments.tableTitle": { + "defaultMessage": "Assessments Statistics ({numStudents} students)" }, - "course.statistics.StatisticsIndex.course.StudentPerformanceTable.descending": { - "defaultMessage": "Descending" + "course.statistics.StatisticsIndex.assessments.title": { + "defaultMessage": "Title" + }, + "course.statistics.StatisticsIndex.course.StudentPerformanceTable.achievementCountDetails": { + "defaultMessage": "No. of Achievements (Total: {courseAchievementCount})" + }, + "course.statistics.StatisticsIndex.course.StudentPerformanceTable.correctness": { + "defaultMessage": "Correctness" }, "course.statistics.StatisticsIndex.course.StudentPerformanceTable.experiencePoints": { "defaultMessage": "Experience Points" @@ -6025,32 +6948,23 @@ "defaultMessage": "Tutors" }, "course.statistics.StatisticsIndex.course.StudentPerformanceTable.highlight": { - "defaultMessage": "Highlight top and bottom {percent}%" + "defaultMessage": "Highlight top and bottom {percent}% based on {criteria}" }, "course.statistics.StatisticsIndex.course.StudentPerformanceTable.learningRate": { "defaultMessage": "Learning Rate" }, - "course.statistics.StatisticsIndex.course.StudentPerformanceTable.learningRateHint": { - "defaultMessage": "A learning rate of 200% means that they can complete the course in half the time." - }, - "course.statistics.StatisticsIndex.course.StudentPerformanceTable.level": { + "course.statistics.StatisticsIndex.course.StudentPerformanceTable.levelInfo": { "defaultMessage": "Level (Max: {maxLevel})" }, - "course.statistics.StatisticsIndex.course.StudentPerformanceTable.levelFilter": { - "defaultMessage": "Level: {name}" - }, "course.statistics.StatisticsIndex.course.StudentPerformanceTable.name": { "defaultMessage": "Name" }, "course.statistics.StatisticsIndex.course.StudentPerformanceTable.noData": { "defaultMessage": "No Data" }, - "course.statistics.StatisticsIndex.course.StudentPerformanceTable.numSubmissions": { + "course.statistics.StatisticsIndex.course.StudentPerformanceTable.numSubmissionsDetails": { "defaultMessage": "No. of Submissions (Total: {courseAssessmentCount})" }, - "course.statistics.StatisticsIndex.course.StudentPerformanceTable.phantom": { - "defaultMessage": "Include phantom users" - }, "course.statistics.StatisticsIndex.course.StudentPerformanceTable.studentType": { "defaultMessage": "Student Type" }, @@ -6060,27 +6974,15 @@ "course.statistics.StatisticsIndex.course.StudentPerformanceTable.studentType.phantom": { "defaultMessage": "Phantom" }, - "course.statistics.StatisticsIndex.course.StudentPerformanceTable.tableTitle": { - "defaultMessage": "Students Sorted in {direction} {column}" - }, "course.statistics.StatisticsIndex.course.StudentPerformanceTable.title": { "defaultMessage": "Student Performance" }, - "course.statistics.StatisticsIndex.course.StudentPerformanceTable.tutorFilter": { - "defaultMessage": "Tutor: {name}" - }, - "course.statistics.StatisticsIndex.course.StudentPerformanceTable.videoPercentWatched": { - "defaultMessage": "Video % Count" - }, "course.statistics.StatisticsIndex.course.StudentPerformanceTable.videoPercentWatchedHeader": { "defaultMessage": "Average Video % Watched" }, "course.statistics.StatisticsIndex.course.StudentPerformanceTable.videoSubmissionCountHeader": { "defaultMessage": "Videos Watched (Total: {courseVideoCount})" }, - "course.statistics.StatisticsIndex.course.searchBar": { - "defaultMessage": "Search by Student Name" - }, "course.statistics.StatisticsIndex.course.StudentProgressionChart.deadlines": { "defaultMessage": "Deadlines" }, @@ -6108,86 +7010,23 @@ "course.statistics.StatisticsIndex.course.StudentProgressionChart.xAxisLabel": { "defaultMessage": "Date" }, - "course.statistics.StatisticsIndex.course.StudentProgressionChart.yAxisLabel": { - "defaultMessage": "Assessment (Sorted by Deadline)" - }, - "course.statistics.StatisticsIndex.course.error": { - "defaultMessage": "Something went wrong when fetching course statistics! Please refresh to try again." - }, - "course.statistics.StatisticsIndex.course.performanceError": { - "defaultMessage": "Something went wrong when fetching course performance statistics! Please refresh to try again." - }, - "course.statistics.StatisticsIndex.course.progressionError": { - "defaultMessage": "Something went wrong when fetching course progression statistics! Please refresh to try again." - }, - "course.statistics.StatisticsIndex.header.statistics": { - "defaultMessage": "Statistics" - }, - "course.statistics.StatisticsIndex.assessments.title": { - "defaultMessage": "Title" - }, - "course.statistics.StatisticsIndex.assessments.startAt": { - "defaultMessage": "Starts At" - }, - "course.statistics.StatisticsIndex.assessments.tab": { - "defaultMessage": "Tab" - }, - "course.statistics.StatisticsIndex.assessments.category": { - "defaultMessage": "Category" - }, - "course.statistics.StatisticsIndex.assessments.numSubmittedStudents": { - "defaultMessage": "# Submitted" - }, - "course.statistics.StatisticsIndex.assessments.numAttemptedStudents": { - "defaultMessage": "# Attempted" - }, - "course.statistics.StatisticsIndex.assessments.numLateStudents": { - "defaultMessage": "# Late" - }, - "course.statistics.StatisticsIndex.assessments.averageGrade": { - "defaultMessage": "Avg Grade" - }, - "course.statistics.StatisticsIndex.assessments.stdevGrade": { - "defaultMessage": "Stdev Grade" - }, - "course.statistics.StatisticsIndex.assessments.averageTimeTaken": { - "defaultMessage": "Avg Time" - }, - "course.statistics.StatisticsIndex.assessments.stdevTimeTaken": { - "defaultMessage": "Stdev Time" - }, - "course.statistics.StatisticsIndex.assessments.tableTitle": { - "defaultMessage": "Assessments Statistics ({numStudents} students)" - }, - "course.statistics.StatisticsIndex.assessments.csvFileTitle": { - "defaultMessage": "Assessments Statistics" - }, - "course.statistics.StatisticsIndex.assessments.searchBar": { - "defaultMessage": "Search by Assessment Title, Tab, or Category" - }, - "course.statistics.StatisticsIndex.assessments.selectedNUsers": { - "defaultMessage": "Download Score Summary for {numUsers} students?" - }, - "course.statistics.StatisticsIndex.assessments.downloadCsv": { - "defaultMessage": "Download" - }, - "course.statistics.StatisticsIndex.assessments.downloadScoreSummary": { - "defaultMessage": "Download Score Summary for the following Assessments?" + "course.statistics.StatisticsIndex.course.StudentProgressionChart.yAxisLabel": { + "defaultMessage": "Assessment (Sorted by Deadline)" }, - "course.statistics.StatisticsIndex.assessments.downloadScoreSummarySuccess": { - "defaultMessage": "Successfully downloaded score summary" + "course.statistics.StatisticsIndex.course.csvFileTitle": { + "defaultMessage": "Student Performance Statistics" }, - "course.statistics.StatisticsIndex.assessments.downloadScoreSummaryFailure": { - "defaultMessage": "An error occurred while downloading score summary" + "course.statistics.StatisticsIndex.course.searchBar": { + "defaultMessage": "Search by Student Name" }, - "course.statistics.StatisticsIndex.assessments.downloadScoreSummaryPending": { - "defaultMessage": "Your download is being processed. Please wait." + "course.statistics.StatisticsIndex.header.statistics": { + "defaultMessage": "Statistics" }, "course.statistics.StatisticsIndex.staff.averageMarkingTime": { "defaultMessage": "Avg Time / Assessment" }, - "course.statistics.StatisticsIndex.staff.error": { - "defaultMessage": "Something went wrong when fetching staff statistics! Please refresh to try again." + "course.statistics.StatisticsIndex.staff.csvFileTitle": { + "defaultMessage": "Staff Statistics" }, "course.statistics.StatisticsIndex.staff.name": { "defaultMessage": "Name" @@ -6198,23 +7037,20 @@ "course.statistics.StatisticsIndex.staff.numStudents": { "defaultMessage": "# Students" }, + "course.statistics.StatisticsIndex.staff.searchBar": { + "defaultMessage": "Search by Staff Name" + }, "course.statistics.StatisticsIndex.staff.stddev": { - "defaultMessage": "Standard Deviation" + "defaultMessage": "Stdev Time / Assessment" }, "course.statistics.StatisticsIndex.staff.tableTitle": { "defaultMessage": "Staff Statistics" }, - "course.statistics.StatisticsIndex.staff.csvFileTitle": { - "defaultMessage": "Staff Statistics" - }, - "course.statistics.StatisticsIndex.staff.searchBar": { - "defaultMessage": "Search by Staff Name" - }, - "course.statistics.StatisticsIndex.staffFailure": { - "defaultMessage": "Failed to fetch staff data!" + "course.statistics.StatisticsIndex.students.csvFileTitle": { + "defaultMessage": "Student Statistics" }, - "course.statistics.StatisticsIndex.students.error": { - "defaultMessage": "Something went wrong when fetching student statistics! Please refresh to try again." + "course.statistics.StatisticsIndex.students.email": { + "defaultMessage": "Email" }, "course.statistics.StatisticsIndex.students.experiencePoints": { "defaultMessage": "Experience Points" @@ -6228,14 +7064,8 @@ "course.statistics.StatisticsIndex.students.name": { "defaultMessage": "Name" }, - "course.statistics.StatisticsIndex.students.email": { - "defaultMessage": "Email" - }, - "course.statistics.StatisticsIndex.students.noStudents": { - "defaultMessage": "There is no student in this course, yet..." - }, - "course.statistics.StatisticsIndex.students.showMyStudentsOnly": { - "defaultMessage": "Show My Students Only" + "course.statistics.StatisticsIndex.students.searchBar": { + "defaultMessage": "Search by Student Name or Student Type" }, "course.statistics.StatisticsIndex.students.studentsType": { "defaultMessage": "Student Type" @@ -6243,21 +7073,12 @@ "course.statistics.StatisticsIndex.students.tableTitle": { "defaultMessage": "Student Statistics ({numStudents} students, {numPhantom} phantom)" }, - "course.statistics.StatisticsIndex.students.tutorFilter": { - "defaultMessage": "Tutor: {name}" - }, "course.statistics.StatisticsIndex.students.videoPercentWatched": { "defaultMessage": "Average % Watched" }, "course.statistics.StatisticsIndex.students.videoSubmissionCount": { "defaultMessage": "Videos Watched (Total: {courseVideoCount})" }, - "course.statistics.StatisticsIndex.students.csvFileTitle": { - "defaultMessage": "Student Statistics" - }, - "course.statistics.StatisticsIndex.students.searchBar": { - "defaultMessage": "Search by Student Name or Student Type" - }, "course.statistics.StatisticsIndex.studentsFailure": { "defaultMessage": "Failed to fetch student data!" }, @@ -6282,12 +7103,6 @@ "course.statistics.course.studentProgressionChart.startAt": { "defaultMessage": "Starts at: {startAt}" }, - "course.statistics.failures.coursePerformance": { - "defaultMessage": "Failed to fetch course performance data!" - }, - "course.statistics.failures.courseProgression": { - "defaultMessage": "Failed to fetch course progression data!" - }, "course.statistics.tabs.course": { "defaultMessage": "Course" }, @@ -6297,6 +7112,15 @@ "course.statistics.tabs.courseProgression": { "defaultMessage": "Course Progression" }, + "course.stories.CikgoErrorPage.errorFetching": { + "defaultMessage": "Either it's supposed to be naught, or something went wrong." + }, + "course.stories.CikgoErrorPage.errorFetchingDescription": { + "defaultMessage": "Cikgo is our partner that powers this experience. They were contactable, but did not give us any resources for this request just now. Please try again later, and if this persists, contact us." + }, + "course.stories.pages.MissionControlPage": { + "defaultMessage": "Mission Control" + }, "course.survey.DeleteSectionButton.deleteSection": { "defaultMessage": "Delete Section" }, @@ -6784,7 +7608,7 @@ "defaultMessage": "Revert and delete timeline and its times" }, "course.timelines.defaultTimeline": { - "defaultMessage": "Default" + "defaultMessage": "Default Timeline" }, "course.timelines.deleteTime": { "defaultMessage": "Delete time" @@ -6942,6 +7766,9 @@ "course.userInvitation.InviteUsersRegistrationCode.registrationCodeNote": { "defaultMessage": "Users who have been invited and use this invitation code to register for the course would not have the proper status reflected in the Invitations page." }, + "course.userInvitations.IndividualInvitations.addRowsByEmail": { + "defaultMessage": "Add Rows by Email" + }, "course.userInvitations.IndividualInvitations.appendNewRow": { "defaultMessage": "Add Row" }, @@ -6951,12 +7778,39 @@ "course.userInvitations.IndividualInvitations.invite": { "defaultMessage": "Invite All Users" }, + "course.userInvitations.IndividualInvitations.malformedEmail": { + "defaultMessage": "{n, plural, one {This email is } other {These emails are }} wrongly formatted: {emails}" + }, + "course.userInvitations.IndividualInvitations.nameEmailInput": { + "defaultMessage": "John Doe '; \"Doe, Jane\" '; ..." + }, "course.userInvitations.IndividualInvitations.namePlaceholder": { "defaultMessage": "Awesome User" }, "course.userInvitations.IndividualInvitations.removeInvitation": { "defaultMessage": "Remove Invitation" }, + "course.userInvitations.InvitationActionButtons.deletionConfirm": { + "defaultMessage": "Are you sure you wish to delete invitation to {name} ({email})?" + }, + "course.userInvitations.InvitationActionButtons.deletionFailure": { + "defaultMessage": "Failed to delete user - {error}" + }, + "course.userInvitations.InvitationActionButtons.deletionSuccess": { + "defaultMessage": "Invitation for {name} was deleted." + }, + "course.userInvitations.InvitationActionButtons.deletionTooltip": { + "defaultMessage": "Delete Invitation" + }, + "course.userInvitations.InvitationActionButtons.resendFailure": { + "defaultMessage": "Failed to resend invitation - {error}" + }, + "course.userInvitations.InvitationActionButtons.resendSuccess": { + "defaultMessage": "Resent email invitation to {email}!" + }, + "course.userInvitations.InvitationActionButtons.resendTooltip": { + "defaultMessage": "Resend Invitation" + }, "course.userInvitations.InvitationResultDialog.body": { "defaultMessage": "{newInvitationsCount, plural, =0 {No new users were} one {# new user has been} other {# new users have been}} invited to Coursemology. {newCourseUsersCount, plural, =0 {No user with Coursemology account has been} one {# new user with existing Coursemology account has been} other {# new users with existing Coursemology accounts have been}} added to this course." }, @@ -6990,9 +7844,6 @@ "course.userInvitations.InvitationResultDialog.newInvitations": { "defaultMessage": "New Invitations ({count})" }, - "course.userInvitations.InvitationsBarChart.accepted": { - "defaultMessage": "Accepted Invitations" - }, "course.userInvitations.InvitationsIndex.failure": { "defaultMessage": "Failed to fetch all invitations" }, @@ -7050,27 +7901,6 @@ "course.userInvitations.InviteUsersfileUploadForm.invite": { "defaultMessage": "Invite Users from File" }, - "course.userInvitations.InvitationActionButtons.deletionConfirm": { - "defaultMessage": "Are you sure you wish to delete invitation to {name} ({email})?" - }, - "course.userInvitations.InvitationActionButtons.deletionFailure": { - "defaultMessage": "Failed to delete user - {error}" - }, - "course.userInvitations.InvitationActionButtons.deletionSuccess": { - "defaultMessage": "Invitation for {name} was deleted." - }, - "course.userInvitations.InvitationActionButtons.deletionTooltip": { - "defaultMessage": "Delete Invitation" - }, - "course.userInvitations.InvitationActionButtons.resendFailure": { - "defaultMessage": "Failed to resend invitation - {error}" - }, - "course.userInvitations.InvitationActionButtons.resendSuccess": { - "defaultMessage": "Resent email invitation to {email}!" - }, - "course.userInvitations.InvitationActionButtons.resendTooltip": { - "defaultMessage": "Resend Invitation" - }, "course.userInvitations.RegistrationCodeButton.registrationCode": { "defaultMessage": "Registration Code" }, @@ -7089,6 +7919,9 @@ "course.userInvitations.UserInvitationsTable.accepted": { "defaultMessage": "Accepted" }, + "course.userInvitations.UserInvitationsTable.confirmedTooltip": { + "defaultMessage": "Accepted {confirmedAt}" + }, "course.userInvitations.UserInvitationsTable.failed": { "defaultMessage": "Failed" }, @@ -7101,9 +7934,6 @@ "course.userInvitations.UserInvitationsTable.sentTooltip": { "defaultMessage": "Sent {sentAt}" }, - "course.userInvitations.UserInvitationsTable.confirmedTooltip": { - "defaultMessage": "Accepted {confirmedAt}" - }, "course.userNotification.AchievementGainedPopup.unlocked": { "defaultMessage": "Achievement Unlocked!" }, @@ -7122,12 +7952,6 @@ "course.users.ExperiencePointsRecords.experiencePointsHistoryHeader": { "defaultMessage": "Experience Points History: {for}" }, - "course.users.ExperiencePointsRecords.fetchUsersFailure": { - "defaultMessage": "Failed to fetch records" - }, - "course.users.ExperiencePointsTable.fetchRecordsFailure": { - "defaultMessage": "Failed to fetch records" - }, "course.users.ManageStaff.noStaff": { "defaultMessage": "No staff in course." }, @@ -7185,6 +8009,15 @@ "course.users.ManageUsersTable.defaultTimeline": { "defaultMessage": "Default" }, + "course.users.ManageUsersTable.deletionConfirm": { + "defaultMessage": "Are you sure you wish to delete {role} {name} ({email})?" + }, + "course.users.ManageUsersTable.deletionFailure": { + "defaultMessage": "Failed to delete {role} {name} ({email})." + }, + "course.users.ManageUsersTable.deletionScheduled": { + "defaultMessage": "{role} {name} ({email}) has been scheduled for deletion." + }, "course.users.ManageUsersTable.group": { "defaultMessage": "Group: {name}" }, @@ -7200,6 +8033,24 @@ "course.users.ManageUsersTable.selectedNUsers": { "defaultMessage": "Selected {n, plural, =1 {# user} other {# users}}" }, + "course.users.ManageUsersTable.suspend": { + "defaultMessage": "Suspend" + }, + "course.users.ManageUsersTable.suspendFailure": { + "defaultMessage": "Failed to suspend {name}." + }, + "course.users.ManageUsersTable.suspendSuccess": { + "defaultMessage": "{name} is now suspended. They cannot access this course until they are unsuspended." + }, + "course.users.ManageUsersTable.unsuspend": { + "defaultMessage": "Unsuspend" + }, + "course.users.ManageUsersTable.unsuspendFailure": { + "defaultMessage": "Failed to unsuspend {name}." + }, + "course.users.ManageUsersTable.unsuspendSuccess": { + "defaultMessage": "{name} is no longer suspended. They can now access the course." + }, "course.users.ManageUsersTable.updateFailure": { "defaultMessage": "Failed to update user - {error}" }, @@ -7308,36 +8159,6 @@ "course.users.UpgradeToStaff.upgradeSuccess": { "defaultMessage": "{count, plural, =0 {No users were} one {# new user has} other {# new users have}} been upgraded to {role}" }, - "course.users.ManageUsersTable.deletionConfirm": { - "defaultMessage": "Are you sure you wish to delete {role} {name} ({email})?" - }, - "course.users.ManageUsersTable.deletionFailure": { - "defaultMessage": "Failed to delete user." - }, - "course.users.ManageUsersTable.deletionScheduled": { - "defaultMessage": "{role} {name} ({email}) has been scheduled for deletion." - }, - "course.users.ManageUsersTable.deletionSuccess": { - "defaultMessage": "User was deleted." - }, - "course.users.ManageUsersTable.suspend": { - "defaultMessage": "Suspend" - }, - "course.users.ManageUsersTable.suspendFailure": { - "defaultMessage": "Failed to suspend {name}." - }, - "course.users.ManageUsersTable.suspendSuccess": { - "defaultMessage": "{name} is now suspended. They cannot access this course until they are unsuspended." - }, - "course.users.ManageUsersTable.unsuspend": { - "defaultMessage": "Unsuspend" - }, - "course.users.ManageUsersTable.unsuspendFailure": { - "defaultMessage": "Failed to unsuspend {name}." - }, - "course.users.ManageUsersTable.unsuspendSuccess": { - "defaultMessage": "{name} is no longer suspended. They can now access the course." - }, "course.users.UserManagementTabs.enrolRequestsTitle": { "defaultMessage": "Enrol Requests" }, @@ -7476,27 +8297,27 @@ "course.video.VideoShow.videoTitle": { "defaultMessage": "Video - {title}" }, + "course.video.VideoTable.actions": { + "defaultMessage": "Actions" + }, + "course.video.VideoTable.averageWatched": { + "defaultMessage": "Average % Watched" + }, "course.video.VideoTable.noVideo": { "defaultMessage": "No Video" }, - "course.video.VideoTable.title": { - "defaultMessage": "Title" + "course.video.VideoTable.published": { + "defaultMessage": "Published" }, "course.video.VideoTable.startAt": { "defaultMessage": "Start At" }, + "course.video.VideoTable.title": { + "defaultMessage": "Title" + }, "course.video.VideoTable.watchCount": { "defaultMessage": "Watch Count" }, - "course.video.VideoTable.averageWatched": { - "defaultMessage": "Average % Watched" - }, - "course.video.VideoTable.published": { - "defaultMessage": "Published" - }, - "course.video.VideoTable.actions": { - "defaultMessage": "Actions" - }, "course.video.VideosIndex.fetchVideosFailure": { "defaultMessage": "Failed to retrieve videos." }, @@ -7644,9 +8465,33 @@ "course.videoSubmissions.UserVideoSubmissionsIndex.videoSubmissionsHeader": { "defaultMessage": "Video Watch History" }, + "d6avGo": { + "defaultMessage": "Submissions" + }, + "f9aTl7": { + "defaultMessage": "Role-Playing Assessments" + }, + "jvrMfo": { + "defaultMessage": "Assistants" + }, "landing_page.create_an_account": { "defaultMessage": "Create an account" }, + "landing_page.iconEngaging": { + "defaultMessage": "Engaging" + }, + "landing_page.iconEngagingSubtitle": { + "defaultMessage": "It is built for all teachers. You do not need to have any programming knowledge to master the platform. Coursemology is easy and intuitive to use for both teachers and students." + }, + "landing_page.iconGeneral": { + "defaultMessage": "General" + }, + "landing_page.iconGeneralSubtitle": { + "defaultMessage": "It is built for all subjects. The gamification system of Coursemology does not make any assumptions on the subject. Through Coursemology, any teacher who teaches any subject can turn his course exercises into an online game." + }, + "landing_page.iconSimple": { + "defaultMessage": "Simple" + }, "landing_page.new_to_coursemology": { "defaultMessage": "New to Coursemology?" }, @@ -7683,12 +8528,12 @@ "lib.components.core.Expandable.showMore": { "defaultMessage": "Show more" }, - "lib.components.core.Note.noteHeader": { - "defaultMessage": "Note" - }, "lib.components.core.Note.errorHeader": { "defaultMessage": "Error" }, + "lib.components.core.Note.noteHeader": { + "defaultMessage": "Note" + }, "lib.components.core.banners.ServerUnreachableBanner.refreshPage": { "defaultMessage": "Refresh page" }, @@ -7818,6 +8663,69 @@ "lib.components.form.fields.SingleFileInput.removeFile": { "defaultMessage": "Remove File" }, + "lib.components.getHelp.filter.filterAssessmentLabel": { + "defaultMessage": "Filter by Assessment" + }, + "lib.components.getHelp.filter.filterCourseLabel": { + "defaultMessage": "Filter by Course" + }, + "lib.components.getHelp.filter.filterEndDateLabel": { + "defaultMessage": "End Date" + }, + "lib.components.getHelp.filter.filterStartDateLabel": { + "defaultMessage": "Start Date" + }, + "lib.components.getHelp.filter.filterStudentLabel": { + "defaultMessage": "Filter by Student" + }, + "lib.components.getHelp.filter.lastFourteenDays": { + "defaultMessage": "Last 14 Days" + }, + "lib.components.getHelp.filter.lastSevenDays": { + "defaultMessage": "Last 7 Days" + }, + "lib.components.getHelp.filter.lastSixMonths": { + "defaultMessage": "Last 6 Months" + }, + "lib.components.getHelp.filter.lastThirtyDays": { + "defaultMessage": "Last 30 Days" + }, + "lib.components.getHelp.filter.lastTwelveMonths": { + "defaultMessage": "Last 12 Months" + }, + "lib.components.getHelp.header": { + "defaultMessage": "Recent Get Help Activity ({total, plural, one {# Conversation} other {# Conversations}})" + }, + "lib.components.getHelp.table.assessmentTitle": { + "defaultMessage": "Assessment" + }, + "lib.components.getHelp.table.courseTitle": { + "defaultMessage": "Course" + }, + "lib.components.getHelp.table.createdAt": { + "defaultMessage": "Last Message At" + }, + "lib.components.getHelp.table.instanceTitle": { + "defaultMessage": "Instance" + }, + "lib.components.getHelp.table.lastMessage": { + "defaultMessage": "Last Message" + }, + "lib.components.getHelp.table.messageCount": { + "defaultMessage": "# Msgs" + }, + "lib.components.getHelp.table.questionNumber": { + "defaultMessage": "Question" + }, + "lib.components.getHelp.table.studentName": { + "defaultMessage": "Name" + }, + "lib.components.getHelp.validation.exceedDateRange": { + "defaultMessage": "Date range cannot exceed 365 days" + }, + "lib.components.getHelp.validation.invalidDateSelection": { + "defaultMessage": "End Date must be after or equal to Start Date" + }, "lib.components.navigation.AdminPopupMenuList.adminPanel": { "defaultMessage": "System Admin Panel" }, @@ -7854,6 +8762,24 @@ "lib.components.navigation.CourseSwitcherPopupMenu.thisCourse": { "defaultMessage": "This course" }, + "lib.components.table.MuiColumnPickerDialog.apply": { + "defaultMessage": "Apply to view" + }, + "lib.components.table.MuiColumnPickerDialog.cancel": { + "defaultMessage": "Cancel" + }, + "lib.components.table.MuiColumnPickerDialog.defaultTitle": { + "defaultMessage": "Select columns" + }, + "lib.components.table.MuiColumnPickerDialog.export": { + "defaultMessage": "Apply and Export" + }, + "lib.components.table.MuiTableToolbar.directExport": { + "defaultMessage": "Export" + }, + "lib.components.table.MuiTableToolbar.exportTrigger": { + "defaultMessage": "Export…" + }, "lib.hooks.router.usePrompt.sureYouWantToLeave": { "defaultMessage": "Are you sure you want to leave this page? You will lose unsaved changes." }, @@ -7881,108 +8807,27 @@ "lib.translations.beta": { "defaultMessage": "Beta" }, - "lib.components.getHelp.header": { - "defaultMessage": "Recent Get Help Activity ({total, plural, one {# Conversation} other {# Conversations}})" - }, - "lib.components.getHelp.filter.filterCourseLabel": { - "defaultMessage": "Filter by Course" - }, - "lib.components.getHelp.filter.filterAssessmentLabel": { - "defaultMessage": "Filter by Assessment" - }, - "lib.components.getHelp.filter.filterStudentLabel": { - "defaultMessage": "Filter by Student" - }, - "lib.components.getHelp.filter.filterStartDateLabel": { - "defaultMessage": "Start Date" - }, - "lib.components.getHelp.filter.filterEndDateLabel": { - "defaultMessage": "End Date" - }, - "lib.components.getHelp.filter.lastSevenDays": { - "defaultMessage": "Last 7 Days" - }, - "lib.components.getHelp.filter.lastFourteenDays": { - "defaultMessage": "Last 14 Days" - }, - "lib.components.getHelp.filter.lastThirtyDays": { - "defaultMessage": "Last 30 Days" - }, - "lib.components.getHelp.filter.lastSixMonths": { - "defaultMessage": "Last 6 Months" - }, - "lib.components.getHelp.filter.lastTwelveMonths": { - "defaultMessage": "Last 12 Months" - }, - "lib.components.getHelp.table.studentName": { - "defaultMessage": "Name" - }, - "lib.components.getHelp.table.messageCount": { - "defaultMessage": "# Msgs" - }, - "lib.components.getHelp.table.lastMessage": { - "defaultMessage": "Last Message" - }, - "lib.components.getHelp.table.questionNumber": { - "defaultMessage": "Question" - }, - "lib.components.getHelp.table.assessmentTitle": { - "defaultMessage": "Assessment" - }, - "lib.components.getHelp.table.createdAt": { - "defaultMessage": "Last Message At" - }, - "lib.components.getHelp.table.courseTitle": { - "defaultMessage": "Course" - }, - "lib.components.getHelp.table.instanceTitle": { - "defaultMessage": "Instance" - }, - "lib.components.getHelp.validation.invalidDateSelection": { - "defaultMessage": "End Date must be after or equal to Start Date" - }, - "lib.components.getHelp.validation.exceedDateRange": { - "defaultMessage": "Date range cannot exceed 365 days" - }, - "lib.components.table.MuiColumnPickerDialog.apply": { - "defaultMessage": "Apply to view" - }, - "lib.components.table.MuiColumnPickerDialog.cancel": { - "defaultMessage": "Cancel" - }, - "lib.components.table.MuiColumnPickerDialog.defaultTitle": { - "defaultMessage": "Select columns" - }, - "lib.components.table.MuiColumnPickerDialog.export": { - "defaultMessage": "Apply and Export" - }, - "lib.components.table.MuiTableToolbar.directExport": { - "defaultMessage": "Export" - }, - "lib.components.table.MuiTableToolbar.exportTrigger": { - "defaultMessage": "Export…" - }, "lib.translations.course.users.fetchUsersFailure": { "defaultMessage": "Failed to fetch users." }, "lib.translations.course.users.manageUsersHeader": { "defaultMessage": "Manage Users" }, - "lib.translations.course.users.roles.student": { - "defaultMessage": "Student" - }, - "lib.translations.course.users.roles.teachingAssistant": { - "defaultMessage": "Teaching Assistant" + "lib.translations.course.users.roles.manager": { + "defaultMessage": "Manager" }, "lib.translations.course.users.roles.observer": { "defaultMessage": "Observer" }, - "lib.translations.course.users.roles.manager": { - "defaultMessage": "Manager" - }, "lib.translations.course.users.roles.owner": { "defaultMessage": "Owner" }, + "lib.translations.course.users.roles.student": { + "defaultMessage": "Student" + }, + "lib.translations.course.users.roles.teachingAssistant": { + "defaultMessage": "Teaching Assistant" + }, "lib.translations.experimental": { "defaultMessage": "Experimental" }, @@ -8094,14 +8939,17 @@ "lib.translations.form.validation.startEndDateValidationError": { "defaultMessage": "Must be after Start Date" }, - "lib.translations.instance.users.roles.normal": { - "defaultMessage": "Normal" + "lib.translations.instance.users.roles.administrator": { + "defaultMessage": "Administrator" }, "lib.translations.instance.users.roles.instructor": { "defaultMessage": "Instructor" }, - "lib.translations.instance.users.roles.administrator": { - "defaultMessage": "Administrator" + "lib.translations.instance.users.roles.normal": { + "defaultMessage": "Normal" + }, + "lib.translations.messages.featureUnavailable": { + "defaultMessage": "This feature is currently unavailable." }, "lib.translations.messages.fetchingError": { "defaultMessage": "An error occurred when loading your data. Please reload and try again." @@ -8112,9 +8960,27 @@ "lib.translations.messages.loadImageError": { "defaultMessage": "An error occurred when loading your image. Please try selecting another one." }, + "lib.translations.myStudents": { + "defaultMessage": "My Students" + }, + "lib.translations.myStudentsIncludingPhantoms": { + "defaultMessage": "My Students (Including Phantoms)" + }, "lib.translations.no": { "defaultMessage": "No" }, + "lib.translations.staff": { + "defaultMessage": "Staff" + }, + "lib.translations.staffIncludingPhantoms": { + "defaultMessage": "Staff (Including Phantoms)" + }, + "lib.translations.students": { + "defaultMessage": "Students" + }, + "lib.translations.studentsIncludingPhantoms": { + "defaultMessage": "Students (Including Phantoms)" + }, "lib.translations.summary": { "defaultMessage": "Summary" }, @@ -8280,33 +9146,15 @@ "lib.translations.yes": { "defaultMessage": "Yes" }, - "material.attemptLoader.errorAccessingMaterial": { - "defaultMessage": "An error occurred while accessing this material. Try again later." - }, - "system.admin.instance.instance.InstanceAdminNavigator.announcements": { - "defaultMessage": "Announcements" - }, - "system.admin.instance.instance.InstanceAdminNavigator.components": { - "defaultMessage": "Components" - }, - "system.admin.instance.instance.InstanceAdminNavigator.courses": { - "defaultMessage": "Courses" - }, - "system.admin.instance.instance.InstanceAdminNavigator.roleRequests": { - "defaultMessage": "Role Requests" - }, - "system.admin.instance.instance.InstanceAdminNavigator.users": { - "defaultMessage": "Users" - }, - "system.admin.instance.instance.InstanceAdminNavigator.getHelp": { - "defaultMessage": "Get Help" - }, "system.admin.admin.AdminNavigator.announcements": { "defaultMessage": "System Announcements" }, "system.admin.admin.AdminNavigator.courses": { "defaultMessage": "Courses" }, + "system.admin.admin.AdminNavigator.getHelp": { + "defaultMessage": "Get Help" + }, "system.admin.admin.AdminNavigator.instances": { "defaultMessage": "Instances" }, @@ -8316,9 +9164,6 @@ "system.admin.admin.AdminNavigator.users": { "defaultMessage": "Users" }, - "system.admin.admin.AdminNavigator.getHelp": { - "defaultMessage": "Get Help" - }, "system.admin.admin.AnnouncementsIndex.fetchAnnouncementsFailure": { "defaultMessage": "Unable to fetch announcements" }, @@ -8403,21 +9248,24 @@ "system.admin.admin.InstancesTable.updateSuccess": { "defaultMessage": "Renamed {field} from {prevValue} to {newValue}" }, + "system.admin.admin.UsersButton.associatedCourses": { + "defaultMessage": "{courseName} ({instanceName})" + }, "system.admin.admin.UsersButton.deleteTooltip": { "defaultMessage": "Delete User" }, "system.admin.admin.UsersButton.deletionConfirm": { - "defaultMessage": "Are you sure you wish to delete {role} {name} ({email})?" + "defaultMessage": "Are you sure you wish to proceed?" }, "system.admin.admin.UsersButton.deletionConfirmTitle": { "defaultMessage": "Deleting {role} User {name} ({email})" }, - "system.admin.admin.UsersButton.deletionPromptContent": { - "defaultMessage": "Deleting this user will PERMANENTLY delete associated data in the following {count, plural, one {course} other {courses}}:" - }, "system.admin.admin.UsersButton.deletionFailure": { "defaultMessage": "Failed to delete user - {error}" }, + "system.admin.admin.UsersButton.deletionPromptContent": { + "defaultMessage": "Deleting this user will PERMANENTLY delete associated data in the following {count, plural, one {course} other {courses}}:" + }, "system.admin.admin.UsersButton.deletionSuccess": { "defaultMessage": "User was deleted." }, @@ -8463,6 +9311,24 @@ "system.admin.instance.instance.IndividualInvitations.invite": { "defaultMessage": "Invite All Users" }, + "system.admin.instance.instance.InstanceAdminNavigator.announcements": { + "defaultMessage": "Announcements" + }, + "system.admin.instance.instance.InstanceAdminNavigator.components": { + "defaultMessage": "Components" + }, + "system.admin.instance.instance.InstanceAdminNavigator.courses": { + "defaultMessage": "Courses" + }, + "system.admin.instance.instance.InstanceAdminNavigator.getHelp": { + "defaultMessage": "Get Help" + }, + "system.admin.instance.instance.InstanceAdminNavigator.roleRequests": { + "defaultMessage": "Role Requests" + }, + "system.admin.instance.instance.InstanceAdminNavigator.users": { + "defaultMessage": "Users" + }, "system.admin.instance.instance.InstanceAnnouncementsIndex.fetchAnnouncementsFailure": { "defaultMessage": "Unable to fetch announcements" }, @@ -8556,21 +9422,12 @@ "system.admin.instance.instance.InstanceUsersIndex.totalUsers": { "defaultMessage": "Total Users: {allCount} ({adminCount} Administrators, {instructorCount} Instructors, {normalCount} Normal)" }, - "system.admin.instance.instance.InstanceUsersInvitations.accepted": { - "defaultMessage": "Accepted Invitations" - }, - "system.admin.instance.instance.InstanceUsersInvitations.failed": { - "defaultMessage": "Failed Invitations" - }, "system.admin.instance.instance.InstanceUsersInvitations.fetch.failure": { "defaultMessage": "Failed to fetch invitations." }, "system.admin.instance.instance.InstanceUsersInvitations.header": { "defaultMessage": "Invitations" }, - "system.admin.instance.instance.InstanceUsersInvitations.pending": { - "defaultMessage": "Pending Invitations" - }, "system.admin.instance.instance.InstanceUsersInvitations.title": { "defaultMessage": "Users" }, @@ -8583,6 +9440,27 @@ "system.admin.instance.instance.InstanceUsersTabs.usersTab": { "defaultMessage": "Users" }, + "system.admin.instance.instance.InvitationActionButtons.deletionConfirm": { + "defaultMessage": "Are you sure you wish to delete invitation to {name} ({email})?" + }, + "system.admin.instance.instance.InvitationActionButtons.deletionFailure": { + "defaultMessage": "Failed to delete user - {error}" + }, + "system.admin.instance.instance.InvitationActionButtons.deletionSuccess": { + "defaultMessage": "Invitation for {name} was deleted." + }, + "system.admin.instance.instance.InvitationActionButtons.deletionTooltip": { + "defaultMessage": "Delete Invitation" + }, + "system.admin.instance.instance.InvitationActionButtons.resendFailure": { + "defaultMessage": "Failed to resend invitation - {error}" + }, + "system.admin.instance.instance.InvitationActionButtons.resendSuccess": { + "defaultMessage": "Resent email invitation to {email}!" + }, + "system.admin.instance.instance.InvitationActionButtons.resendTooltip": { + "defaultMessage": "Resend Invitation" + }, "system.admin.instance.instance.InvitationResultDialog.close": { "defaultMessage": "Close" }, @@ -8613,27 +9491,6 @@ "system.admin.instance.instance.InvitationResultDialog.newInvitations": { "defaultMessage": "New Invitations ({count})" }, - "system.admin.instance.instance.InvitationActionButtons.deletionConfirm": { - "defaultMessage": "Are you sure you wish to delete invitation to {name} ({email})?" - }, - "system.admin.instance.instance.InvitationActionButtons.deletionFailure": { - "defaultMessage": "Failed to delete user - {error}" - }, - "system.admin.instance.instance.InvitationActionButtons.deletionSuccess": { - "defaultMessage": "Invitation for {name} was deleted." - }, - "system.admin.instance.instance.InvitationActionButtons.deletionTooltip": { - "defaultMessage": "Delete Invitation" - }, - "system.admin.instance.instance.InvitationActionButtons.resendFailure": { - "defaultMessage": "Failed to resend invitation - {error}" - }, - "system.admin.instance.instance.InvitationActionButtons.resendSuccess": { - "defaultMessage": "Resent email invitation to {email}!" - }, - "system.admin.instance.instance.InvitationActionButtons.resendTooltip": { - "defaultMessage": "Resend Invitation" - }, "system.admin.instance.instance.PendingRoleRequestsButton.approveFailure": { "defaultMessage": "Failed to approve role request - {error}" }, @@ -8679,6 +9536,9 @@ "system.admin.instance.instance.UserInvitationsTable.accepted": { "defaultMessage": "Accepted" }, + "system.admin.instance.instance.UserInvitationsTable.confirmedTooltip": { + "defaultMessage": "Accepted {confirmedAt}" + }, "system.admin.instance.instance.UserInvitationsTable.failed": { "defaultMessage": "Failed" }, @@ -8691,9 +9551,6 @@ "system.admin.instance.instance.UserInvitationsTable.sentTooltip": { "defaultMessage": "Sent {sentAt}" }, - "system.admin.instance.instance.UserInvitationsTable.confirmedTooltip": { - "defaultMessage": "Accepted {confirmedAt}" - }, "system.admin.instance.instance.UsersButton.deleteTooltip": { "defaultMessage": "Remove User" }, @@ -8703,12 +9560,12 @@ "system.admin.instance.instance.UsersButton.deletionConfirmTitle": { "defaultMessage": "Removing {role} User {name} ({email})" }, - "system.admin.instance.instance.UsersButton.deletionPromptContent": { - "defaultMessage": "Removing this user may cause errors in the following {count, plural, one {course} other {courses}}:" - }, "system.admin.instance.instance.UsersButton.deletionFailure": { "defaultMessage": "Failed to remove user - {error}" }, + "system.admin.instance.instance.UsersButton.deletionPromptContent": { + "defaultMessage": "Removing this user may cause errors in the following {count, plural, one {course} other {courses}}:" + }, "system.admin.instance.instance.UsersButton.deletionSuccess": { "defaultMessage": "User was removed from this instance." }, @@ -9038,5 +9895,8 @@ }, "users.troubleSigningIn": { "defaultMessage": "Trouble signing in?" + }, + "wEQDC6": { + "defaultMessage": "Edit" } }