Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sort spaced practice #3824

Merged
merged 4 commits into from
Nov 5, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions tutor/src/models/student-tasks/step.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,14 @@ const NO_ADDITIONAL_CONTENT = [
'external_url',
];

export enum GroupType {
Unknown = 'unknown_group',
Fixed = 'fixed_group',
SpacedPractice = 'spaced_practice_group',
Personalized = 'personalized_group',
Recovery = 'recovery_group',
}

export class StudentTaskStep extends BaseModel {

@field id = NEW_ID;
Expand Down
65 changes: 51 additions & 14 deletions tutor/src/models/task-plans/teacher/scores.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,13 @@ import type {
TaskPlanType, TeacherTaskPlan,
} from '../../../models'
import {
DroppedQuestion, GradingTemplate, CoursePeriod, currentExercises,
DroppedQuestion, GradingTemplate, CoursePeriod, currentExercises, Exercise,
} from '../../../models'
import { GroupType } from '../../../models/student-tasks/step'

export class TaskPlanScoreStudentQuestion extends BaseModel {
@field question_id?:ID;
@field exercise_id?:ID;
@field question_id?: ID;
@field exercise_id?: ID;
@field is_completed = false;
@field points?: number;
@field comments = ''
Expand Down Expand Up @@ -141,7 +142,7 @@ export class TaskPlanScoreStudent extends BaseModel {
@field late_work_point_penalty = 0
@field grades_need_publishing = false

@model(TaskPlanScoreStudentQuestion) questions:TaskPlanScoreStudentQuestion[] = [];
@model(TaskPlanScoreStudentQuestion) questions: TaskPlanScoreStudentQuestion[] = [];

get tasking() { return getParentOf<TaskPlanScoresTasking>(this) }

Expand All @@ -156,12 +157,12 @@ export class TaskPlanScoreStudent extends BaseModel {
* except that repeats are not allowed
*/
@computed get questionHeadings() {
const usedHeadings:TaskPlanScoreHeading[] = [];
const usedHeadings: TaskPlanScoreHeading[] = [];

return this.questions.map((question, questionIdx) => {
const heading = this.tasking.question_headings.slice(questionIdx).find(
heading => heading.question_ids.includes(question.question_id || 0) &&
!usedHeadings.includes(heading)
!usedHeadings.includes(heading)
)

if (!heading) { return null; }
Expand Down Expand Up @@ -199,7 +200,7 @@ export class TaskPlanScoreStudent extends BaseModel {
}

@computed get extension() {
if(isEmpty(this.tasking.scores.taskPlan.extensions)) return null;
if (isEmpty(this.tasking.scores.taskPlan.extensions)) return null;
return this.tasking.scores.taskPlan.extensions.find(ex => ex.role_id == this.role_id);
}

Expand All @@ -225,6 +226,7 @@ export class TaskPlanScoreHeading extends BaseModel {
@field points = 0;
@field points_without_dropping = 0;
@field ecosystem_id = NEW_ID;
@field group_type: GroupType = GroupType.Unknown;

exercises = currentExercises

Expand All @@ -239,6 +241,14 @@ export class TaskPlanScoreHeading extends BaseModel {
return 'Tutor' !== this.type;
}

@computed get isPersonalized() {
return this.group_type === GroupType.Personalized
}

@computed get isSpacedPractice() {
return this.group_type === GroupType.SpacedPractice
}

@computed get index() {
return this.tasking && this.tasking.question_headings.indexOf(this);
}
Expand All @@ -261,7 +271,7 @@ export class TaskPlanScoreHeading extends BaseModel {
if (this.question_ids.length != 1) { return null; }

return this.exercise &&
this.exercise.content.questions.find(q => q.id == this.question_ids[0]);
this.exercise.content.questions.find(q => q.id == this.question_ids[0]);
}

@computed get questionIdsSet() {
Expand Down Expand Up @@ -374,6 +384,26 @@ export class TaskPlanScoreHeading extends BaseModel {
}
}

type QuestionInfo = {
availablePoints: number,
averagePoints: number,
completed: number,
droppedQuestion: DroppedQuestion,
exercise: Exercise,
hasFreeResponse: boolean,
heading: TaskPlanScoreHeading,
id: ID,
index: number,
isCore: boolean,
isSpacedPractice: boolean,
key: number,
points: number,
question: TaskPlanScoreStudentQuestion,
remaining: number,
responses: TaskPlanScoreStudentQuestion[],
totalPoints: number,
}

export class TaskPlanScoresTasking extends BaseModel {
@field id = NEW_ID;
@field period_id: ID = NEW_ID;
Expand Down Expand Up @@ -436,6 +466,7 @@ export class TaskPlanScoresTasking extends BaseModel {
remaining: heading ? heading.gradedStats.remaining : 0,
index: studentQuestion.index,
isCore: heading?.isCore,
isSpacedPractice: heading?.isSpacedPractice,
droppedQuestion: droppedQuestion,
heading,
exercise,
Expand All @@ -450,7 +481,7 @@ export class TaskPlanScoresTasking extends BaseModel {
// add their stats once all the questions are gathered
return sortBy(Object.values(info).map((qi: any) => {
for (const answer of qi.question.answers) {
answer.selected_count = filter(qi.responses, r => r.selected_answer_id == answer.id).length,
answer.selected_count = filter(qi.responses, r => r.selected_answer_id == answer.id).length;
answer.answered_count = qi.responses.length;
}
return {
Expand All @@ -459,7 +490,7 @@ export class TaskPlanScoresTasking extends BaseModel {
completed: filter(qi.responses, 'is_completed').length,
points: sumBy(qi.responses, 'points'),
totalPoints: qi.points * qi.responses.length,
};
} as QuestionInfo;
}), 'index');
}

Expand Down Expand Up @@ -509,7 +540,7 @@ export class TaskPlanScoresTasking extends BaseModel {

@computed get hasFinishedGrading() {
const completedWrmQuestions = filter(this.wrmQuestions, s => s.is_completed);
if(completedWrmQuestions.length <= 0) { return false; }
if (completedWrmQuestions.length <= 0) { return false; }
return every(completedWrmQuestions, q => !q.needs_grading);
}

Expand All @@ -518,14 +549,20 @@ export class TaskPlanScoresTasking extends BaseModel {
return some(wrmQuestions, q => q.is_completed);
}

@computed get groupQuestionsByPageTopic() {
const questions = this.questionsInfo;
@computed get questionsGroupedByPageTopic() {
// Filter out spaced practice so they can be shown at the end of the QuestionList
const questions = this.questionsInfo.filter(i => !i.heading.isSpacedPractice);

//order the questions by the exercise page's chapter_section so that the first chapters are shown first
const sortedQuestions = orderBy(questions, ['exercise.page.chapter_section.asNumber'], ['asc']);
return groupBy(sortedQuestions, q => {
return q.exercise.page.title;
});
}

@computed get spacedPracticeQuestions() {
return this.questionsInfo.filter(i => i.heading.isSpacedPractice)
}
}

export class TaskPlanScores extends BaseModel {
Expand All @@ -545,7 +582,7 @@ export class TaskPlanScores extends BaseModel {
modelize(this);
}

@model(DroppedQuestion) dropped_questions:DroppedQuestion[] = []
@model(DroppedQuestion) dropped_questions: DroppedQuestion[] = []

@model(TaskPlanScoresTasking) tasking_plans = array((taskings: TaskPlanScoresTasking[]) => ({
forPeriod(period: CoursePeriod) { return find(taskings, { period_id: period.id }); },
Expand Down
104 changes: 63 additions & 41 deletions tutor/src/screens/assignment-review/overview.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ const QuestionHeader = observer(({ ux, styleVariant, label, info }) => (
QuestionHeader.propTypes = {
styleVariant: PropTypes.string.isRequired,
label: PropTypes.string.isRequired,
info: PropTypes.object.isRequired,
info: PropTypes.object.isRequired,
};

const QuestionFooter = observer(({ ux, info }) => {
Expand All @@ -94,33 +94,33 @@ const QuestionFooter = observer(({ ux, info }) => {
Average score: {info.averagePoints ? ScoresHelper.formatPoints(info.averagePoints) : 'n/a'}
</strong>
{ux.canDisplayGradingButton &&
<GradeButton
className={cn('btn btn-new-flag',
{
'btn-primary': !ux.scores.hasFinishedGrading,
'btn-standard': !ux.scores.hasFinishedGrading,
'btn-new-flag': !ux.scores.hasFinishedGrading,
'btn-link': ux.scores.hasFinishedGrading,
})}
to="gradeAssignmentQuestion"
params={{
courseId: ux.course.id,
id: ux.planId,
periodId: ux.selectedPeriod.id,
questionId: `${info.id}`,
}}
displayingFlag={displayingFlag}
>
{displayingFlag && <span className="flag">{info.remaining} NEW</span>}
<span>{ux.scores.hasFinishedGrading ? 'Regrade' : 'Grade Answers' }</span>
</GradeButton>
<GradeButton
className={cn('btn btn-new-flag',
{
'btn-primary': !ux.scores.hasFinishedGrading,
'btn-standard': !ux.scores.hasFinishedGrading,
'btn-new-flag': !ux.scores.hasFinishedGrading,
'btn-link': ux.scores.hasFinishedGrading,
})}
to="gradeAssignmentQuestion"
params={{
courseId: ux.course.id,
id: ux.planId,
periodId: ux.selectedPeriod.id,
questionId: `${info.id}`,
}}
displayingFlag={displayingFlag}
>
{displayingFlag && <span className="flag">{info.remaining} NEW</span>}
<span>{ux.scores.hasFinishedGrading ? 'Regrade' : 'Grade Answers'}</span>
</GradeButton>
}
</Footer>);
});
QuestionHeader.propTypes = {
styleVariant: PropTypes.string.isRequired,
label: PropTypes.string.isRequired,
info: PropTypes.object.isRequired,
info: PropTypes.object.isRequired,
};

const StyledQuestionFreeResponse = styled.div`
Expand Down Expand Up @@ -281,10 +281,10 @@ const WRQFreeResponse = observer(({ ux, info }) => {
<div className="grade">
{response.needs_grading && 'Not graded'}
{!isNaN(response.grader_points) &&
<div>
<h3>{ScoresHelper.formatPoints(response.grader_points)}</h3>
{response.grader_comments}
</div>}
<div>
<h3>{ScoresHelper.formatPoints(response.grader_points)}</h3>
{response.grader_comments}
</div>}
</div>
</StyledQuestionFreeResponse>
</ResponseWrapper>
Expand Down Expand Up @@ -422,7 +422,7 @@ const GradingBlock = observer(({ ux }) => {
}}
>
{ux.gradeableQuestionCount > 0 &&
<span className="flag">{ux.gradeableQuestionCount} NEW</span>}
<span className="flag">{ux.gradeableQuestionCount} NEW</span>}
<span>Grade answers</span>
</GradeButton>
<p>This assignment is now open for grading.</p>
Expand Down Expand Up @@ -510,8 +510,11 @@ HomeworkQuestionsWrapper.propTypes = {
questionsInfo: PropTypes.any.isRequired,
};

const QuestionList = observer(({ ux, scores }) => {
if (!ux.isExercisesReady) { return <Loading message="Loading Questions…"/>; }
const QuestionList = observer(({ ux }) => {
const { scores } = ux

if (!ux.isExercisesReady) { return <Loading message="Loading Questions…" />; }
if (!scores) { return null }

if (scores.questionsInfo && scores.questionsInfo.length === 0) {
return (
Expand All @@ -522,32 +525,51 @@ const QuestionList = observer(({ ux, scores }) => {
);
}

if(ux.planScores.isReading) {
return Object.keys(scores.groupQuestionsByPageTopic).map((key, index) => (
if (ux.planScores.isReading) {
const personalizedQuestions = Object.keys(scores.questionsGroupedByPageTopic).map((key, index) => (
<div key={index}>
<StyledTopicHeader>
<h3>
<ArbitraryHtmlAndMath
html={key} />
<ArbitraryHtmlAndMath html={key} />
</h3>
{/** Only the show the button at the top with the very first header */}
{
index === 0 &&
<NamesToogleButton ux={ux}/>
<NamesToogleButton ux={ux} />
}
</StyledTopicHeader>
<HomeworkQuestionsWrapper
questionsInfo={scores.groupQuestionsByPageTopic[key]}
questionsInfo={scores.questionsGroupedByPageTopic[key]}
ux={ux}
/>
</div>
));
))


const spacedPracticeQuestions = (
<div key="spaced-practice">
<StyledTopicHeader>
<h3>Spaced Practice</h3>
</StyledTopicHeader>
<HomeworkQuestionsWrapper
questionsInfo={scores.spacedPracticeQuestions}
ux={ux}
/>
</div>
)

return (
<>
{personalizedQuestions}
{scores.spacedPracticeQuestions.length > 0 && spacedPracticeQuestions}
</>
);
}

return (
<>
<StyledNamesToogleButtonWrapper>
<NamesToogleButton ux={ux}/>
<NamesToogleButton ux={ux} />
</StyledNamesToogleButtonWrapper>
<HomeworkQuestionsWrapper
questionsInfo={scores.questionsInfo}
Expand All @@ -565,7 +587,7 @@ QuestionList.propTypes = {

const HomeWorkInfo = observer(({ ux }) => (
<>
<GradingBlock ux={ux}/>
<GradingBlock ux={ux} />
<StyledStickyTable>
<Row>
<Header>Question Number</Header>
Expand All @@ -574,8 +596,8 @@ const HomeWorkInfo = observer(({ ux }) => (
<DroppedQuestionHeadingIndicator heading={h} preventOverflow={false} />
{h.isCore ?
<StyledButton variant="link" onClick={() => ux.scrollToQuestion(h.question_id, i)}>
{i+1}
</StyledButton> : i+1}
{i + 1}
</StyledButton> : i + 1}
</Header>)}
</Row>
<Row>
Expand Down Expand Up @@ -618,7 +640,7 @@ const Overview = observer(({ ux }) => {
return (
<Wrapper data-test-id="overview">
{ux.planScores.isHomework && <HomeWorkInfo ux={ux} />}
<QuestionList ux={ux} scores={ux.scores} />
<QuestionList ux={ux} />
<DropQuestionModal ux={ux} />
</Wrapper>
);
Expand Down