Fix self-evalution checkboxes

This commit is contained in:
Daniel Egger 2022-09-28 16:19:01 +02:00
parent e230c0b8e5
commit bdae082550
11 changed files with 84 additions and 62 deletions

View File

@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { Circle } from '@/services/circle' import type { Circle } from '@/services/circle'
import ItFullScreenModal from '@/components/ui/ItFullScreenModal.vue' import ItFullScreenModal from '@/components/ui/ItFullScreenModal.vue'
const props = defineProps<{ const props = defineProps<{

View File

@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import ItCheckbox from '@/components/ui/ItCheckbox.vue' import ItCheckbox from '@/components/ui/ItCheckbox.vue'
import type { LearningContent, LearningSequence } from '@/types' import type { CourseCompletionStatus, LearningContent, LearningSequence } from '@/types'
import { useCircleStore } from '@/stores/circle' import { useCircleStore } from '@/stores/circle'
import { computed } from 'vue' import { computed } from 'vue'
import _ from 'lodash' import _ from 'lodash'
@ -14,7 +14,11 @@ const props = defineProps<{
const circleStore = useCircleStore() const circleStore = useCircleStore()
function toggleCompleted(learningContent: LearningContent) { function toggleCompleted(learningContent: LearningContent) {
circleStore.markCompletion(learningContent, !learningContent.completed) let completionStatus: CourseCompletionStatus = 'success'
if (learningContent.completion_status === 'success') {
completionStatus = 'fail'
}
circleStore.markCompletion(learningContent, completionStatus)
} }
const someFinished = computed(() => { const someFinished = computed(() => {
@ -36,7 +40,7 @@ const allFinished = computed(() => {
const continueTranslationKeyTuple = computed(() => { const continueTranslationKeyTuple = computed(() => {
if (props.learningSequence && circleStore.circle) { if (props.learningSequence && circleStore.circle) {
const lastFinished = _.findLast(circleStore.circle.flatLearningContents, (learningContent) => { const lastFinished = _.findLast(circleStore.circle.flatLearningContents, (learningContent) => {
return learningContent.completed return learningContent.completion_status === 'success'
}) })
if (!lastFinished) { if (!lastFinished) {
@ -91,7 +95,7 @@ const learningSequenceBorderClass = computed(() => {
class="flex gap-4 pb-3 lg:pb-6" class="flex gap-4 pb-3 lg:pb-6"
> >
<ItCheckbox <ItCheckbox
:modelValue="learningContent.completed" :modelValue="learningContent.completion_status === 'success'"
:onToggle="() => toggleCompleted(learningContent)" :onToggle="() => toggleCompleted(learningContent)"
:data-cy="`${learningContent.slug}`" :data-cy="`${learningContent.slug}`"
> >

View File

@ -2,7 +2,7 @@
import * as log from 'loglevel' import * as log from 'loglevel'
import { computed, reactive } from 'vue' import { computed, reactive } from 'vue'
import { useCircleStore } from '@/stores/circle' import { useCircleStore } from '@/stores/circle'
import { LearningUnit } from '@/types' import type { LearningUnit } from '@/types'
log.debug('LearningContent.vue setup') log.debug('LearningContent.vue setup')
@ -62,24 +62,24 @@ function handleContinue() {
<div class="mt-4 lg:mt-8 flex flex-col lg:flex-row justify-between gap-6"> <div class="mt-4 lg:mt-8 flex flex-col lg:flex-row justify-between gap-6">
<button <button
@click="circleStore.markCompletion(currentQuestion, true)" @click="circleStore.markCompletion(currentQuestion, 'success')"
class="flex-1 inline-flex items-center text-left p-4 border" class="flex-1 inline-flex items-center text-left p-4 border"
:class="{ :class="{
'border-green-500': currentQuestion.completed, 'border-green-500': currentQuestion.completion_status === 'success',
'border-2': currentQuestion.completed, 'border-2': currentQuestion.completion_status === 'success',
'border-gray-500': !currentQuestion.completed, 'border-gray-500': currentQuestion.completion_status !== 'success',
}" }"
> >
<it-icon-smiley-happy class="w-16 h-16 mr-4"></it-icon-smiley-happy> <it-icon-smiley-happy class="w-16 h-16 mr-4"></it-icon-smiley-happy>
<span class="font-bold text-xl"> Ja, ich kann das. </span> <span class="font-bold text-xl"> Ja, ich kann das. </span>
</button> </button>
<button <button
@click="circleStore.markCompletion(currentQuestion, false)" @click="circleStore.markCompletion(currentQuestion, 'fail')"
class="flex-1 inline-flex items-center text-left p-4 border" class="flex-1 inline-flex items-center text-left p-4 border"
:class="{ :class="{
'border-orange-500': currentQuestion.completed === false, 'border-orange-500': currentQuestion.completion_status === 'fail',
'border-2': currentQuestion.completed === false, 'border-2': currentQuestion.completion_status === 'fail',
'border-gray-500': currentQuestion.completed === true || currentQuestion.completed === undefined, 'border-gray-500': currentQuestion.completion_status !== 'fail'
}" }"
> >
<it-icon-smiley-thinking class="w-16 h-16 mr-4"></it-icon-smiley-thinking> <it-icon-smiley-thinking class="w-16 h-16 mr-4"></it-icon-smiley-thinking>

View File

@ -1,13 +1,14 @@
import type { import type {
CircleChild, CircleChild,
CircleCompletion,
CircleGoal, CircleGoal,
CircleJobSituation, CircleJobSituation,
CourseCompletion,
CourseCompletionStatus,
CourseWagtailPage,
LearningContent, LearningContent,
LearningSequence, LearningSequence,
LearningUnit, LearningUnit,
LearningUnitQuestion, LearningUnitQuestion,
CourseWagtailPage,
} from '@/types' } from '@/types'
import type { LearningPath } from '@/services/learningPath' import type { LearningPath } from '@/services/learningPath'
@ -23,6 +24,7 @@ function _createEmptyLearningUnit(parentLearningSequence: LearningSequence): Lea
parentLearningSequence: parentLearningSequence, parentLearningSequence: parentLearningSequence,
children: [], children: [],
last: true, last: true,
completion_status: 'unknown',
} }
} }
@ -112,7 +114,7 @@ export function parseLearningSequences (circle: Circle, children: CircleChild[])
export class Circle implements CourseWagtailPage { export class Circle implements CourseWagtailPage {
readonly type = 'learnpath.Circle'; readonly type = 'learnpath.Circle';
readonly learningSequences: LearningSequence[]; readonly learningSequences: LearningSequence[];
readonly completed: boolean; completion_status: CourseCompletionStatus = 'unknown'
nextCircle?: Circle; nextCircle?: Circle;
previousCircle?: Circle; previousCircle?: Circle;
@ -129,7 +131,6 @@ export class Circle implements CourseWagtailPage {
public readonly parentLearningPath?: LearningPath, public readonly parentLearningPath?: LearningPath,
) { ) {
this.learningSequences = parseLearningSequences(this, this.children); this.learningSequences = parseLearningSequences(this, this.children);
this.completed = false;
} }
public static fromJson(json: any, learningPath?: LearningPath): Circle { public static fromJson(json: any, learningPath?: LearningPath): Circle {
@ -187,7 +188,7 @@ export class Circle implements CourseWagtailPage {
public someFinishedInLearningSequence(translationKey: string): boolean { public someFinishedInLearningSequence(translationKey: string): boolean {
if (translationKey) { if (translationKey) {
return this.flatChildren.filter((lc) => { return this.flatChildren.filter((lc) => {
return lc.completed && lc.parentLearningSequence?.translation_key === translationKey; return lc.completion_status === 'success' && lc.parentLearningSequence?.translation_key === translationKey;
}).length > 0; }).length > 0;
} }
@ -197,7 +198,7 @@ export class Circle implements CourseWagtailPage {
public allFinishedInLearningSequence(translationKey: string): boolean { public allFinishedInLearningSequence(translationKey: string): boolean {
if (translationKey) { if (translationKey) {
const finishedContents = this.flatChildren.filter((lc) => { const finishedContents = this.flatChildren.filter((lc) => {
return lc.completed && lc.parentLearningSequence?.translation_key === translationKey; return lc.completion_status === 'success' && lc.parentLearningSequence?.translation_key === translationKey;
}).length; }).length;
const totalContents = this.flatChildren.filter((lc) => { const totalContents = this.flatChildren.filter((lc) => {
@ -209,15 +210,15 @@ export class Circle implements CourseWagtailPage {
return false; return false;
} }
public parseCompletionData(completionData: CircleCompletion[]) { public parseCompletionData(completionData: CourseCompletion[]) {
this.flatChildren.forEach((page) => { this.flatChildren.forEach((page) => {
const pageIndex = completionData.findIndex((e) => { const pageIndex = completionData.findIndex((e) => {
return e.page_key === page.translation_key; return e.page_key === page.translation_key;
}); });
if (pageIndex >= 0) { if (pageIndex >= 0) {
page.completed = completionData[pageIndex].completed; page.completion_status = completionData[pageIndex].completion_status;
} else { } else {
page.completed = undefined; page.completion_status = 'unknown';
} }
}); });

View File

@ -1,11 +1,18 @@
import * as _ from 'lodash' import * as _ from 'lodash'
import type { CircleCompletion, LearningContent, LearningPathChild, CourseWagtailPage, Topic } from '@/types' import type {
CourseCompletion,
CourseCompletionStatus,
CourseWagtailPage,
LearningContent,
LearningPathChild,
Topic,
} from '@/types'
import { Circle } from '@/services/circle' import { Circle } from '@/services/circle'
function getLastCompleted(learningPathKey: string, completionData: CircleCompletion[]) { function getLastCompleted(courseId: number, completionData: CourseCompletion[]) {
return _.orderBy(completionData, ['updated_at'], 'desc').find((c: CircleCompletion) => { return _.orderBy(completionData, ['updated_at'], 'desc').find((c: CourseCompletion) => {
return c.completed && c.learning_path_key === learningPathKey && c.page_type === 'learnpath.LearningContent' return c.completion_status === 'success' && c.course === courseId && c.page_type === 'learnpath.LearningContent'
}) })
} }
@ -14,9 +21,10 @@ export class LearningPath implements CourseWagtailPage {
public topics: Topic[] public topics: Topic[]
public circles: Circle[] public circles: Circle[]
public nextLearningContent?: LearningContent public nextLearningContent?: LearningContent
readonly completion_status: CourseCompletionStatus = 'unknown'
public static fromJson(json: any, completionData: CircleCompletion[]): LearningPath { public static fromJson(json: any, completionData: CourseCompletion[]): LearningPath {
return new LearningPath(json.id, json.slug, json.title, json.translation_key, json.children, completionData) return new LearningPath(json.id, json.slug, json.title, json.translation_key, json.course.id, json.children, completionData)
} }
constructor( constructor(
@ -24,8 +32,9 @@ export class LearningPath implements CourseWagtailPage {
public readonly slug: string, public readonly slug: string,
public readonly title: string, public readonly title: string,
public readonly translation_key: string, public readonly translation_key: string,
public readonly courseId: number,
public children: LearningPathChild[], public children: LearningPathChild[],
completionData?: any completionData?: CourseCompletion[]
) { ) {
// parse children // parse children
this.topics = [] this.topics = []
@ -42,7 +51,9 @@ export class LearningPath implements CourseWagtailPage {
} }
if (page.type === 'learnpath.Circle') { if (page.type === 'learnpath.Circle') {
const circle = Circle.fromJson(page, this) const circle = Circle.fromJson(page, this)
circle.parseCompletionData(completionData) if (completionData) {
circle.parseCompletionData(completionData)
}
if (topic) { if (topic) {
topic.circles.push(circle) topic.circles.push(circle)
} }
@ -59,17 +70,21 @@ export class LearningPath implements CourseWagtailPage {
this.topics.push(topic) this.topics.push(topic)
} }
this.calcNextLearningContent(completionData) if (completionData) {
this.calcNextLearningContent(completionData)
}
} }
public calcNextLearningContent(completionData: CircleCompletion[]): void { public calcNextLearningContent(completionData: CourseCompletion[]): void {
this.nextLearningContent = undefined this.nextLearningContent = undefined
const lastCompletedLearningContent = getLastCompleted(this.translation_key, completionData) const lastCompletedLearningContent = getLastCompleted(this.courseId, completionData)
if (lastCompletedLearningContent) { if (lastCompletedLearningContent) {
const lastCircle = this.circles.find( const lastCircle = this.circles.find(
(circle) => circle.translation_key === lastCompletedLearningContent.circle_key (circle) => {
return circle.flatLearningContents.find((learningContent) => learningContent.translation_key === lastCompletedLearningContent.page_key)
}
) )
if (lastCircle) { if (lastCircle) {
const lastLearningContent = lastCircle.flatLearningContents.find( const lastLearningContent = lastCircle.flatLearningContents.find(

View File

@ -2,7 +2,7 @@ import * as log from 'loglevel'
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import type { LearningContent, LearningUnit, LearningUnitQuestion } from '@/types' import type { CourseCompletionStatus, LearningContent, LearningUnit, LearningUnitQuestion } from '@/types'
import type { Circle } from '@/services/circle' import type { Circle } from '@/services/circle'
import { itPost } from '@/fetchHelpers' import { itPost } from '@/fetchHelpers'
import { useLearningPathStore } from '@/stores/learningPath' import { useLearningPathStore } from '@/stores/learningPath'
@ -61,12 +61,12 @@ export const useCircleStore = defineStore({
return learningUnit return learningUnit
}, },
async markCompletion(page: LearningContent | LearningUnitQuestion, flag = true) { async markCompletion(page: LearningContent | LearningUnitQuestion, completion_status:CourseCompletionStatus='success') {
try { try {
page.completed = flag; page.completion_status = completion_status;
const completionData = await itPost('/api/completion/circle/mark/', { const completionData = await itPost('/api/course/completion/mark/', {
page_key: page.translation_key, page_key: page.translation_key,
completed: page.completed, completion_status: page.completion_status,
}); });
if (this.circle) { if (this.circle) {
this.circle.parseCompletionData(completionData); this.circle.parseCompletionData(completionData);
@ -100,10 +100,10 @@ export const useCircleStore = defineStore({
}, },
calcSelfEvaluationStatus(learningUnit: LearningUnit) { calcSelfEvaluationStatus(learningUnit: LearningUnit) {
if (learningUnit.children.length > 0) { if (learningUnit.children.length > 0) {
if (learningUnit.children.every((q) => q.completed)) { if (learningUnit.children.every((q) => q.completion_status === 'success')) {
return true; return true;
} }
if (learningUnit.children.some((q) => q.completed !== undefined)) { if (learningUnit.children.some((q) => q.completion_status === 'fail')) {
return false; return false;
} }
} }
@ -111,7 +111,7 @@ export const useCircleStore = defineStore({
}, },
continueFromLearningContent(currentLearningContent: LearningContent) { continueFromLearningContent(currentLearningContent: LearningContent) {
if (currentLearningContent) { if (currentLearningContent) {
this.markCompletion(currentLearningContent, true); this.markCompletion(currentLearningContent, 'success');
const nextLearningContent = currentLearningContent.nextLearningContent; const nextLearningContent = currentLearningContent.nextLearningContent;
const currentParent = currentLearningContent.parentLearningUnit; const currentParent = currentLearningContent.parentLearningUnit;

View File

@ -20,7 +20,7 @@ export const useLearningPathStore = defineStore({
return this.learningPath; return this.learningPath;
} }
const learningPathData = await itGet(`/api/course/page/${slug}/`); const learningPathData = await itGet(`/api/course/page/${slug}/`);
const completionData = await itGet(`/api/completion/learning_path/${learningPathData.translation_key}/`); const completionData = await itGet(`/api/course/completion/${learningPathData.course.id}/`);
if (!learningPathData) { if (!learningPathData) {
throw `No learning path found with: ${slug}`; throw `No learning path found with: ${slug}`;

View File

@ -1,8 +1,17 @@
import type { Circle } from '@/services/circle' import type { Circle } from '@/services/circle'
export type LearningContentType = 'assignment' | 'book' | 'document' | export type CourseCompletionStatus = 'unknown' | 'fail' | 'success'
'exercise' | 'media_library' | 'online_training' |
'resource' | 'test' | 'video'; export type LearningContentType =
| 'assignment'
| 'book'
| 'document'
| 'exercise'
| 'media_library'
| 'online_training'
| 'resource'
| 'test'
| 'video'
export interface LearningContentBlock { export interface LearningContentBlock {
type: LearningContentType type: LearningContentType
@ -111,7 +120,7 @@ export interface CourseWagtailPage {
readonly title: string; readonly title: string;
readonly slug: string; readonly slug: string;
readonly translation_key: string; readonly translation_key: string;
completed?: boolean; completion_status: CourseCompletionStatus;
} }
export interface LearningContent extends CourseWagtailPage { export interface LearningContent extends CourseWagtailPage {
@ -163,17 +172,17 @@ export interface Topic extends CourseWagtailPage {
export type LearningPathChild = Topic | WagtailCircle; export type LearningPathChild = Topic | WagtailCircle;
export interface CircleCompletion { export interface CourseCompletion {
id: number; id: number;
created_at: string; created_at: string;
updated_at: string; updated_at: string;
user: number; user: number;
page_key: string; page_key: string;
page_type: string; page_type: string;
circle_key: string; page_slug: string;
learning_path_key: string; course: number;
completed: boolean; completion_status: CourseCompletionStatus;
json_data: any; additional_json_data: any;
} }
export interface CircleDiagramData { export interface CircleDiagramData {

View File

@ -4,7 +4,7 @@ import { onMounted, reactive, watch } from 'vue'
import { useCircleStore } from '@/stores/circle' import { useCircleStore } from '@/stores/circle'
import { useAppStore } from '@/stores/app' import { useAppStore } from '@/stores/app'
import LearningContent from '@/components/circle/LearningContent.vue' import LearningContent from '@/components/circle/LearningContent.vue'
import { LearningContent as LearningContentType } from '@/types' import type { LearningContent as LearningContentType } from '@/types'
log.debug('LearningContentView created') log.debug('LearningContentView created')

View File

@ -50,11 +50,6 @@ urlpatterns = [
path(r"api/course/completion/mark/", mark_course_completion, name="mark_course_completion"), path(r"api/course/completion/mark/", mark_course_completion, name="mark_course_completion"),
path(r"api/course/completion/<course_id>/", request_course_completion, name="request_course_completion"), path(r"api/course/completion/<course_id>/", request_course_completion, name="request_course_completion"),
# completion
# path(r"api/completion/circle/<uuid:circle_key>/", request_circle_completion, name="request_circle_completion"),
# path(r"api/completion/learning_path/<uuid:learning_path_key>/", request_learning_path_completion, name="request_learning_path_completion"),
# path(r"api/completion/circle/mark/", mark_circle_completion, name="mark_circle_completion"),
# testing and debug # testing and debug
path('server/raise_error/', user_passes_test(lambda u: u.is_superuser, login_url='/login/')(raise_example_error), ), path('server/raise_error/', user_passes_test(lambda u: u.is_superuser, login_url='/login/')(raise_example_error), ),
path("server/checkratelimit/", check_rate_limit), path("server/checkratelimit/", check_rate_limit),

View File

@ -16,8 +16,6 @@ from vbv_lernwelt.learnpath.serializer_helpers import get_it_serializer_class
class LearningPath(Page): class LearningPath(Page):
# PageChooserPanel('related_page', 'demo.PublisherPage'),
content_panels = Page.content_panels content_panels = Page.content_panels
subpage_types = ['learnpath.Circle', 'learnpath.Topic'] subpage_types = ['learnpath.Circle', 'learnpath.Topic']
parent_page_types = ['course.CoursePage'] parent_page_types = ['course.CoursePage']
@ -35,7 +33,7 @@ class LearningPath(Page):
@classmethod @classmethod
def get_serializer_class(cls): def get_serializer_class(cls):
return get_it_serializer_class( return get_it_serializer_class(
cls, ['id', 'title', 'slug', 'type', 'translation_key', 'children'] cls, ['id', 'title', 'slug', 'type', 'translation_key', 'children', 'course']
) )