diff --git a/kolibri/plugins/coach/api.py b/kolibri/plugins/coach/api.py index f640a15512a..2cf5a947254 100644 --- a/kolibri/plugins/coach/api.py +++ b/kolibri/plugins/coach/api.py @@ -324,7 +324,7 @@ def has_permission(self, request, view): return False try: collection = Exam.objects.get(id=exam_id).collection - except Exam.DoesNotExist: + except (Exam.DoesNotExist, ValueError): return False allowed_roles = [role_kinds.ADMIN, role_kinds.COACH] try: diff --git a/kolibri/plugins/coach/assets/src/modules/questionList/index.js b/kolibri/plugins/coach/assets/src/modules/questionList/index.js index 6695a7ef8dd..0149226d06b 100644 --- a/kolibri/plugins/coach/assets/src/modules/questionList/index.js +++ b/kolibri/plugins/coach/assets/src/modules/questionList/index.js @@ -1,3 +1,4 @@ +import { getDifficultQuestions } from '../../utils'; import * as actions from './actions'; function defaultState() { @@ -8,37 +9,13 @@ function defaultState() { }; } -function ratio(stat) { - return stat.correct / stat.total; -} - export default { namespaced: true, state: defaultState(), actions, getters: { difficultQuestions(state) { - return state.itemStats - .filter(stat => { - // Arbitrarily filter out questions that have higher than 80% correct rate - return stat.correct / stat.total < 0.8; - }) - .sort((stat1, stat2) => { - // Sort first by raw correct - if (ratio(stat1) > ratio(stat2)) { - return 1; - } else if (ratio(stat2) > ratio(stat1)) { - return -1; - // If they are equal, prioritize questions in which we have the highest - // number of answers - } else if (stat1.total > stat2.total) { - return -1; - } else if (stat2.total > stat1.total) { - return 1; - } - // Nothing between them! - return 0; - }); + return getDifficultQuestions(state.itemStats); }, }, mutations: { diff --git a/kolibri/plugins/coach/assets/src/routes/planExamRoutes.js b/kolibri/plugins/coach/assets/src/routes/planExamRoutes.js index 1b75a290b67..d5f1a26077b 100644 --- a/kolibri/plugins/coach/assets/src/routes/planExamRoutes.js +++ b/kolibri/plugins/coach/assets/src/routes/planExamRoutes.js @@ -53,7 +53,7 @@ export default [ }, { name: QuizSummaryPage.name, - path: '/:classId/plan/quizzes/:quizId', + path: '/:classId/plan/quizzes/:quizId/:tabId?', component: QuizSummaryPage, meta: { titleParts: ['QUIZ_NAME', 'quizzesLabel', 'CLASS_NAME'], diff --git a/kolibri/plugins/coach/assets/src/utils/index.js b/kolibri/plugins/coach/assets/src/utils/index.js new file mode 100644 index 00000000000..ebcfa0bcd7c --- /dev/null +++ b/kolibri/plugins/coach/assets/src/utils/index.js @@ -0,0 +1,27 @@ +const ratio = question => { + return question.correct / question.total; +}; + +export const getDifficultQuestions = questions => { + return questions + .filter(question => { + // Arbitrarily filter out questions that have higher than 80% correct rate + return question.correct / question.total < 0.8; + }) + .sort((question1, question2) => { + // Sort first by raw correct + if (ratio(question1) > ratio(question2)) { + return 1; + } else if (ratio(question2) > ratio(question1)) { + return -1; + // If they are equal, prioritize questions in which we have the highest + // number of answers + } else if (question1.total > question2.total) { + return -1; + } else if (question2.total > question1.total) { + return 1; + } + // Nothing between them! + return 0; + }); +}; diff --git a/kolibri/plugins/coach/assets/src/views/common/QuizLessonDetailsHeader.vue b/kolibri/plugins/coach/assets/src/views/common/QuizLessonDetailsHeader.vue index 539bbb0eb5d..bc13a4a914d 100644 --- a/kolibri/plugins/coach/assets/src/views/common/QuizLessonDetailsHeader.vue +++ b/kolibri/plugins/coach/assets/src/views/common/QuizLessonDetailsHeader.vue @@ -20,6 +20,7 @@ /> - {{ $tr('reportVisibleToLearnersLabel') }} + {{ $tr('reportVisibleToLearnersLabel') }} + - -
+ +
- {{ coachString('classLabel') }} + {{ coachString('recipientsLabel') }}
- {{ className }} +
- +
- {{ coachString('recipientsLabel') }} + {{ coachString('avgScoreLabel') }} + -
- -
+
- +
- {{ coachString('avgScoreLabel') }} - + {{ coachString('classLabel') }} - +
+ {{ className }} +
@@ -215,7 +219,29 @@ :layout8="{ span: 4 }" :layout12="{ span: 12 }" > -

{{ exam.size_string ? exam.size_string : '--' }}

+ {{ exam.size_string ? exam.size_string : '--' }} + +
+ + +
+ + {{ coreString('dateCreated') }} + + +
@@ -347,6 +373,13 @@ ':hover': { 'background-color': this.$darken1(this.$themePalette.red.v_1100) }, }; }, + examDateCreated() { + if (this.exam.date_created) { + return new Date(this.exam.date_created); + } else { + return null; + } + }, examDateArchived() { if (this.exam.date_archived) { return new Date(this.exam.date_archived); diff --git a/kolibri/plugins/coach/assets/src/views/common/StatusElapsedTime.vue b/kolibri/plugins/coach/assets/src/views/common/StatusElapsedTime.vue index 1cedce05165..10604b587f8 100644 --- a/kolibri/plugins/coach/assets/src/views/common/StatusElapsedTime.vue +++ b/kolibri/plugins/coach/assets/src/views/common/StatusElapsedTime.vue @@ -18,7 +18,7 @@ const HOUR = MINUTE * 60; const DAY = HOUR * 24; - const ACTION_TYPES = ['created', 'closed', 'opened']; + const ACTION_TYPES = ['created', 'closed', 'opened', 'madeVisible']; export default { name: 'StatusElapsedTime', @@ -64,6 +64,8 @@ return this.$tr('closedSecondsAgo', strParams); case 'opened': return this.$tr('openedSecondsAgo', strParams); + case 'madeVisible': + return this.$tr('madeVisibleSecondsAgo', strParams); default: return ''; } @@ -78,6 +80,8 @@ return this.$tr('closedMinutesAgo', strParams); case 'opened': return this.$tr('openedMinutesAgo', strParams); + case 'madeVisible': + return this.$tr('madeVisibleMinutesAgo', strParams); default: return ''; } @@ -92,6 +96,8 @@ return this.$tr('closedHoursAgo', strParams); case 'opened': return this.$tr('openedHoursAgo', strParams); + case 'madeVisible': + return this.$tr('madeVisibleHoursAgo', strParams); default: return ''; } @@ -105,6 +111,8 @@ return this.$tr('closedDaysAgo', strParams); case 'opened': return this.$tr('openedDaysAgo', strParams); + case 'madeVisible': + return this.$tr('madeVisibleDaysAgo', strParams); default: return ''; } @@ -182,6 +190,26 @@ message: 'Started {days} {days, plural, one {day} other {days}} ago', context: 'Indicates that a quiz was started a number of days prior to the current date.', }, + madeVisibleSecondsAgo: { + message: 'Made visible {seconds} {seconds, plural, one {second} other {seconds}} ago', + context: + 'Indicates that a quiz was made visible a number of seconds prior to the current time, but is always less than 1 minute ago.', + }, + madeVisibleMinutesAgo: { + message: 'Made visible {minutes} {minutes, plural, one {minute} other {minutes}} ago', + context: + 'Indicates that a quiz was made visible a number of minutes prior to the current time, but the time is always less than 1 hour ago.', + }, + madeVisibleHoursAgo: { + message: 'Made visible {hours} {hours, plural, one {hour} other {hours}} ago', + context: + 'Indicates that a quiz was made visible a number of hours prior to the current time, but the time is always less than one day ago', + }, + madeVisibleDaysAgo: { + message: 'Made visible {days} {days, plural, one {day} other {days}} ago', + context: + 'Indicates that a quiz was made visible a number of days prior to the current date.', + }, }, }; diff --git a/kolibri/plugins/coach/assets/src/views/plan/QuizSummaryPage/QuizOptionsDropdownMenu.vue b/kolibri/plugins/coach/assets/src/views/plan/QuizSummaryPage/QuizOptionsDropdownMenu.vue index 922bca565d5..81f7cbe1ead 100644 --- a/kolibri/plugins/coach/assets/src/views/plan/QuizSummaryPage/QuizOptionsDropdownMenu.vue +++ b/kolibri/plugins/coach/assets/src/views/plan/QuizSummaryPage/QuizOptionsDropdownMenu.vue @@ -1,9 +1,9 @@ diff --git a/kolibri/plugins/coach/assets/src/views/plan/QuizSummaryPage/api.js b/kolibri/plugins/coach/assets/src/views/plan/QuizSummaryPage/api.js index 69e97909ea6..0351b8195cf 100644 --- a/kolibri/plugins/coach/assets/src/views/plan/QuizSummaryPage/api.js +++ b/kolibri/plugins/coach/assets/src/views/plan/QuizSummaryPage/api.js @@ -1,17 +1,51 @@ import { fetchExamWithContent } from 'kolibri.utils.exams'; import { ExamResource } from 'kolibri.resources'; +import QuizDifficulties from '../../../apiResources/quizDifficulties'; +import { getDifficultQuestions } from '../../../utils'; -export function fetchQuizSummaryPageData(examId) { - return ExamResource.fetchModel({ id: examId }) - .then(exam => { - return fetchExamWithContent(exam); - }) - .then(({ exam, exercises }) => { - return { - exerciseContentNodes: exercises, - exam, - }; - }); +const fetchDifficultQuestions = async exam => { + if (exam.draft) { + return []; + } + const correctnessStats = await QuizDifficulties.fetchDetailCollection( + 'detail', + exam.id, + undefined, + true, + ); + + const allQuestions = exam.question_sources.reduce( + (qs, section) => [...qs, ...section.questions], + [], + ); + + allQuestions.forEach(question => { + const questionStats = correctnessStats.find(stat => stat.item === question.item); + if (questionStats) { + question.correct = questionStats.correct; + question.total = questionStats.total; + } else { + question.correct = 0; + question.total = correctnessStats[0]?.total || 0; + } + question.questionNumber = question.counter_in_exercise; + }); + + return getDifficultQuestions(allQuestions); +}; + +export async function fetchQuizSummaryPageData(examId) { + const _exam = await ExamResource.fetchModel({ id: examId }); + + const { exam, exercises } = await fetchExamWithContent(_exam); + + const difficultQuestions = await fetchDifficultQuestions(exam); + + return { + exam, + exerciseContentNodes: exercises, + difficultQuestions, + }; } export function serverAssignmentPayload(listOfIDs, classId) { diff --git a/kolibri/plugins/coach/assets/src/views/plan/QuizSummaryPage/index.vue b/kolibri/plugins/coach/assets/src/views/plan/QuizSummaryPage/index.vue index ee1db0ddf9c..87fdc014611 100644 --- a/kolibri/plugins/coach/assets/src/views/plan/QuizSummaryPage/index.vue +++ b/kolibri/plugins/coach/assets/src/views/plan/QuizSummaryPage/index.vue @@ -12,6 +12,10 @@ examOrLesson="exam" >