Merged in feature/VBV-459-refactoring-course-completion-rebase-2 (pull request #156)
Feature/VBV-459 refactoring course completion rebase 2 Approved-by: Elia Bieri
This commit is contained in:
commit
24d57577cc
|
|
@ -19,10 +19,10 @@ const props = withDefaults(defineProps<Props>(), {
|
||||||
<div class="flex flex-row items-center">
|
<div class="flex flex-row items-center">
|
||||||
<div v-if="showState" class="mr-4 h-8 w-8">
|
<div v-if="showState" class="mr-4 h-8 w-8">
|
||||||
<it-icon-smiley-happy
|
<it-icon-smiley-happy
|
||||||
v-if="criteria.completion_status === 'success'"
|
v-if="criteria.completion_status === 'SUCCESS'"
|
||||||
></it-icon-smiley-happy>
|
></it-icon-smiley-happy>
|
||||||
<it-icon-smiley-thinking
|
<it-icon-smiley-thinking
|
||||||
v-else-if="criteria.completion_status === 'fail'"
|
v-else-if="criteria.completion_status === 'FAIL'"
|
||||||
></it-icon-smiley-thinking>
|
></it-icon-smiley-thinking>
|
||||||
<it-icon-smiley-neutral v-else></it-icon-smiley-neutral>
|
<it-icon-smiley-neutral v-else></it-icon-smiley-neutral>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -17,9 +17,9 @@ type Story = StoryObj<typeof ItProgress>;
|
||||||
export const NoProgress: Story = {
|
export const NoProgress: Story = {
|
||||||
args: {
|
args: {
|
||||||
statusCount: {
|
statusCount: {
|
||||||
fail: 0,
|
FAIL: 0,
|
||||||
success: 0,
|
SUCCESS: 0,
|
||||||
unknown: 10,
|
UNKNOWN: 10,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
@ -27,9 +27,9 @@ export const NoProgress: Story = {
|
||||||
export const FiftyPrecentSuccessProgress: Story = {
|
export const FiftyPrecentSuccessProgress: Story = {
|
||||||
args: {
|
args: {
|
||||||
statusCount: {
|
statusCount: {
|
||||||
fail: 0,
|
FAIL: 0,
|
||||||
success: 5,
|
SUCCESS: 5,
|
||||||
unknown: 5,
|
UNKNOWN: 5,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
@ -37,9 +37,9 @@ export const FiftyPrecentSuccessProgress: Story = {
|
||||||
export const FiftyPrecentFailProgress: Story = {
|
export const FiftyPrecentFailProgress: Story = {
|
||||||
args: {
|
args: {
|
||||||
statusCount: {
|
statusCount: {
|
||||||
fail: 5,
|
FAIL: 5,
|
||||||
success: 0,
|
SUCCESS: 0,
|
||||||
unknown: 5,
|
UNKNOWN: 5,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from "vue";
|
import { computed } from "vue";
|
||||||
|
|
||||||
export type StatusCountKey = "fail" | "success" | "unknown";
|
export type StatusCountKey = "FAIL" | "SUCCESS" | "UNKNOWN";
|
||||||
export type StatusCount = Record<StatusCountKey, number>;
|
export type StatusCount = Record<StatusCountKey, number>;
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
|
|
@ -10,9 +10,9 @@ const props = defineProps<{
|
||||||
|
|
||||||
const total = computed(() => {
|
const total = computed(() => {
|
||||||
return (
|
return (
|
||||||
(props.statusCount?.fail || 0) +
|
(props.statusCount?.FAIL || 0) +
|
||||||
(props.statusCount?.success || 0) +
|
(props.statusCount?.SUCCESS || 0) +
|
||||||
(props.statusCount?.unknown || 0)
|
(props.statusCount?.UNKNOWN || 0)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -21,7 +21,7 @@ const done = computed(() => {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
return ((props.statusCount?.success || 0) / total.value) * 100;
|
return ((props.statusCount?.SUCCESS || 0) / total.value) * 100;
|
||||||
});
|
});
|
||||||
|
|
||||||
const notDone = computed(() => {
|
const notDone = computed(() => {
|
||||||
|
|
@ -29,7 +29,7 @@ const notDone = computed(() => {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
return ((props.statusCount?.fail || 0) / total.value) * 100 + done.value;
|
return ((props.statusCount?.FAIL || 0) / total.value) * 100 + done.value;
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import type { CourseCompletionStatus } from "@/types";
|
import type { CourseCompletionStatus } from "@/types";
|
||||||
|
|
||||||
export const COMPLETION_SUCCESS: CourseCompletionStatus = "success";
|
export const COMPLETION_SUCCESS: CourseCompletionStatus = "SUCCESS";
|
||||||
export const COMPLETION_FAILURE: CourseCompletionStatus = "fail";
|
export const COMPLETION_FAILURE: CourseCompletionStatus = "FAIL";
|
||||||
export const COMPLETION_UNKNOWN: CourseCompletionStatus = "unknown";
|
export const COMPLETION_UNKNOWN: CourseCompletionStatus = "UNKNOWN";
|
||||||
|
|
|
||||||
|
|
@ -108,6 +108,30 @@ export type AssignmentObjectType = CoursePageInterface & {
|
||||||
translation_key?: Maybe<Scalars['String']['output']>;
|
translation_key?: Maybe<Scalars['String']['output']>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type AttendanceCourseUserMutation = {
|
||||||
|
__typename?: 'AttendanceCourseUserMutation';
|
||||||
|
course_session_attendance_course?: Maybe<CourseSessionAttendanceCourseType>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AttendanceUserInputType = {
|
||||||
|
status: AttendanceUserStatus;
|
||||||
|
user_id: Scalars['ID']['input'];
|
||||||
|
};
|
||||||
|
|
||||||
|
/** An enumeration. */
|
||||||
|
export type AttendanceUserStatus =
|
||||||
|
| 'ABSENT'
|
||||||
|
| 'PRESENT';
|
||||||
|
|
||||||
|
export type AttendanceUserType = {
|
||||||
|
__typename?: 'AttendanceUserType';
|
||||||
|
email?: Maybe<Scalars['String']['output']>;
|
||||||
|
first_name?: Maybe<Scalars['String']['output']>;
|
||||||
|
last_name?: Maybe<Scalars['String']['output']>;
|
||||||
|
status: AttendanceUserStatus;
|
||||||
|
user_id: Scalars['ID']['output'];
|
||||||
|
};
|
||||||
|
|
||||||
/** An enumeration. */
|
/** An enumeration. */
|
||||||
export type CoreUserLanguageChoices =
|
export type CoreUserLanguageChoices =
|
||||||
/** Deutsch */
|
/** Deutsch */
|
||||||
|
|
@ -127,6 +151,19 @@ export type CoursePageInterface = {
|
||||||
translation_key?: Maybe<Scalars['String']['output']>;
|
translation_key?: Maybe<Scalars['String']['output']>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type CourseSessionAttendanceCourseType = {
|
||||||
|
__typename?: 'CourseSessionAttendanceCourseType';
|
||||||
|
attendance_user_list?: Maybe<Array<Maybe<AttendanceUserType>>>;
|
||||||
|
course_session_id?: Maybe<Scalars['ID']['output']>;
|
||||||
|
due_date_id?: Maybe<Scalars['ID']['output']>;
|
||||||
|
end?: Maybe<Scalars['DateTime']['output']>;
|
||||||
|
id: Scalars['ID']['output'];
|
||||||
|
learning_content_id?: Maybe<Scalars['ID']['output']>;
|
||||||
|
location: Scalars['String']['output'];
|
||||||
|
start?: Maybe<Scalars['DateTime']['output']>;
|
||||||
|
trainer: Scalars['String']['output'];
|
||||||
|
};
|
||||||
|
|
||||||
export type CourseType = {
|
export type CourseType = {
|
||||||
__typename?: 'CourseType';
|
__typename?: 'CourseType';
|
||||||
category_name: Scalars['String']['output'];
|
category_name: Scalars['String']['output'];
|
||||||
|
|
@ -186,6 +223,7 @@ export type LearningPathType = CoursePageInterface & {
|
||||||
export type Mutation = {
|
export type Mutation = {
|
||||||
__typename?: 'Mutation';
|
__typename?: 'Mutation';
|
||||||
send_feedback?: Maybe<SendFeedbackPayload>;
|
send_feedback?: Maybe<SendFeedbackPayload>;
|
||||||
|
update_course_session_attendance_course_users?: Maybe<AttendanceCourseUserMutation>;
|
||||||
upsert_assignment_completion?: Maybe<AssignmentCompletionMutation>;
|
upsert_assignment_completion?: Maybe<AssignmentCompletionMutation>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -195,6 +233,12 @@ export type MutationSendFeedbackArgs = {
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type MutationUpdateCourseSessionAttendanceCourseUsersArgs = {
|
||||||
|
attendance_user_list: Array<InputMaybe<AttendanceUserInputType>>;
|
||||||
|
id: Scalars['ID']['input'];
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
export type MutationUpsertAssignmentCompletionArgs = {
|
export type MutationUpsertAssignmentCompletionArgs = {
|
||||||
assignment_id: Scalars['ID']['input'];
|
assignment_id: Scalars['ID']['input'];
|
||||||
assignment_user_id?: InputMaybe<Scalars['ID']['input']>;
|
assignment_user_id?: InputMaybe<Scalars['ID']['input']>;
|
||||||
|
|
@ -216,6 +260,7 @@ export type Query = {
|
||||||
assignment?: Maybe<AssignmentObjectType>;
|
assignment?: Maybe<AssignmentObjectType>;
|
||||||
assignment_completion?: Maybe<AssignmentCompletionObjectType>;
|
assignment_completion?: Maybe<AssignmentCompletionObjectType>;
|
||||||
course?: Maybe<CourseType>;
|
course?: Maybe<CourseType>;
|
||||||
|
course_session_attendance_course?: Maybe<CourseSessionAttendanceCourseType>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -236,6 +281,12 @@ export type QueryCourseArgs = {
|
||||||
id?: InputMaybe<Scalars['Int']['input']>;
|
id?: InputMaybe<Scalars['Int']['input']>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type QueryCourseSessionAttendanceCourseArgs = {
|
||||||
|
assignment_user_id?: InputMaybe<Scalars['ID']['input']>;
|
||||||
|
id: Scalars['ID']['input'];
|
||||||
|
};
|
||||||
|
|
||||||
export type SendFeedbackInput = {
|
export type SendFeedbackInput = {
|
||||||
clientMutationId?: InputMaybe<Scalars['String']['input']>;
|
clientMutationId?: InputMaybe<Scalars['String']['input']>;
|
||||||
course_session: Scalars['Int']['input'];
|
course_session: Scalars['Int']['input'];
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,43 @@
|
||||||
type Query {
|
type Query {
|
||||||
|
course_session_attendance_course(id: ID!, assignment_user_id: ID): CourseSessionAttendanceCourseType
|
||||||
course(id: Int): CourseType
|
course(id: Int): CourseType
|
||||||
assignment(id: ID, slug: String): AssignmentObjectType
|
assignment(id: ID, slug: String): AssignmentObjectType
|
||||||
assignment_completion(assignment_id: ID!, course_session_id: ID!, assignment_user_id: ID): AssignmentCompletionObjectType
|
assignment_completion(assignment_id: ID!, course_session_id: ID!, assignment_user_id: ID): AssignmentCompletionObjectType
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type CourseSessionAttendanceCourseType {
|
||||||
|
id: ID!
|
||||||
|
location: String!
|
||||||
|
trainer: String!
|
||||||
|
course_session_id: ID
|
||||||
|
learning_content_id: ID
|
||||||
|
due_date_id: ID
|
||||||
|
end: DateTime
|
||||||
|
start: DateTime
|
||||||
|
attendance_user_list: [AttendanceUserType]
|
||||||
|
}
|
||||||
|
|
||||||
|
"""
|
||||||
|
The `DateTime` scalar type represents a DateTime
|
||||||
|
value as specified by
|
||||||
|
[iso8601](https://en.wikipedia.org/wiki/ISO_8601).
|
||||||
|
"""
|
||||||
|
scalar DateTime
|
||||||
|
|
||||||
|
type AttendanceUserType {
|
||||||
|
user_id: ID!
|
||||||
|
status: AttendanceUserStatus!
|
||||||
|
first_name: String
|
||||||
|
last_name: String
|
||||||
|
email: String
|
||||||
|
}
|
||||||
|
|
||||||
|
"""An enumeration."""
|
||||||
|
enum AttendanceUserStatus {
|
||||||
|
PRESENT
|
||||||
|
ABSENT
|
||||||
|
}
|
||||||
|
|
||||||
type CourseType {
|
type CourseType {
|
||||||
id: ID!
|
id: ID!
|
||||||
title: String!
|
title: String!
|
||||||
|
|
@ -63,13 +97,6 @@ interface CoursePageInterface {
|
||||||
frontend_url: String
|
frontend_url: String
|
||||||
}
|
}
|
||||||
|
|
||||||
"""
|
|
||||||
The `DateTime` scalar type represents a DateTime
|
|
||||||
value as specified by
|
|
||||||
[iso8601](https://en.wikipedia.org/wiki/ISO_8601).
|
|
||||||
"""
|
|
||||||
scalar DateTime
|
|
||||||
|
|
||||||
type UserType {
|
type UserType {
|
||||||
id: ID!
|
id: ID!
|
||||||
|
|
||||||
|
|
@ -184,6 +211,7 @@ scalar JSONString
|
||||||
|
|
||||||
type Mutation {
|
type Mutation {
|
||||||
send_feedback(input: SendFeedbackInput!): SendFeedbackPayload
|
send_feedback(input: SendFeedbackInput!): SendFeedbackPayload
|
||||||
|
update_course_session_attendance_course_users(attendance_user_list: [AttendanceUserInputType]!, id: ID!): AttendanceCourseUserMutation
|
||||||
upsert_assignment_completion(assignment_id: ID!, assignment_user_id: ID, completion_data_string: String, completion_status: AssignmentCompletionStatus, course_session_id: ID!, evaluation_grade: Float, evaluation_points: Float): AssignmentCompletionMutation
|
upsert_assignment_completion(assignment_id: ID!, assignment_user_id: ID, completion_data_string: String, completion_status: AssignmentCompletionStatus, course_session_id: ID!, evaluation_grade: Float, evaluation_points: Float): AssignmentCompletionMutation
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -220,6 +248,15 @@ input SendFeedbackInput {
|
||||||
clientMutationId: String
|
clientMutationId: String
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AttendanceCourseUserMutation {
|
||||||
|
course_session_attendance_course: CourseSessionAttendanceCourseType
|
||||||
|
}
|
||||||
|
|
||||||
|
input AttendanceUserInputType {
|
||||||
|
user_id: ID!
|
||||||
|
status: AttendanceUserStatus!
|
||||||
|
}
|
||||||
|
|
||||||
type AssignmentCompletionMutation {
|
type AssignmentCompletionMutation {
|
||||||
assignment_completion: AssignmentCompletionObjectType
|
assignment_completion: AssignmentCompletionObjectType
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,14 @@ export const AssignmentCompletionMutation = "AssignmentCompletionMutation";
|
||||||
export const AssignmentCompletionObjectType = "AssignmentCompletionObjectType";
|
export const AssignmentCompletionObjectType = "AssignmentCompletionObjectType";
|
||||||
export const AssignmentCompletionStatus = "AssignmentCompletionStatus";
|
export const AssignmentCompletionStatus = "AssignmentCompletionStatus";
|
||||||
export const AssignmentObjectType = "AssignmentObjectType";
|
export const AssignmentObjectType = "AssignmentObjectType";
|
||||||
|
export const AttendanceCourseUserMutation = "AttendanceCourseUserMutation";
|
||||||
|
export const AttendanceUserInputType = "AttendanceUserInputType";
|
||||||
|
export const AttendanceUserStatus = "AttendanceUserStatus";
|
||||||
|
export const AttendanceUserType = "AttendanceUserType";
|
||||||
export const Boolean = "Boolean";
|
export const Boolean = "Boolean";
|
||||||
export const CoreUserLanguageChoices = "CoreUserLanguageChoices";
|
export const CoreUserLanguageChoices = "CoreUserLanguageChoices";
|
||||||
export const CoursePageInterface = "CoursePageInterface";
|
export const CoursePageInterface = "CoursePageInterface";
|
||||||
|
export const CourseSessionAttendanceCourseType = "CourseSessionAttendanceCourseType";
|
||||||
export const CourseType = "CourseType";
|
export const CourseType = "CourseType";
|
||||||
export const DateTime = "DateTime";
|
export const DateTime = "DateTime";
|
||||||
export const ErrorType = "ErrorType";
|
export const ErrorType = "ErrorType";
|
||||||
|
|
|
||||||
|
|
@ -129,7 +129,7 @@ const assignmentDetail = computed(() =>
|
||||||
</template>
|
</template>
|
||||||
<template #link>
|
<template #link>
|
||||||
<router-link
|
<router-link
|
||||||
v-if="submissionStatusForUser(csu.user_id)?.progressStatus === 'success'"
|
v-if="submissionStatusForUser(csu.user_id)?.progressStatus === 'SUCCESS'"
|
||||||
:to="`/course/${props.courseSession.course.slug}/cockpit/assignment/${assignment.assignmentId}/${csu.user_id}`"
|
:to="`/course/${props.courseSession.course.slug}/cockpit/assignment/${assignment.assignmentId}/${csu.user_id}`"
|
||||||
class="w-full text-right underline"
|
class="w-full text-right underline"
|
||||||
data-cy="show-results"
|
data-cy="show-results"
|
||||||
|
|
|
||||||
|
|
@ -45,10 +45,10 @@ onMounted(async () => {
|
||||||
</div>
|
</div>
|
||||||
<div><ItProgress :status-count="state.progressStatusCount" /></div>
|
<div><ItProgress :status-count="state.progressStatusCount" /></div>
|
||||||
<div class="text-gray-900" :class="{ 'text-gray-900': showTitle }">
|
<div class="text-gray-900" :class="{ 'text-gray-900': showTitle }">
|
||||||
{{ state.progressStatusCount.success || 0 }} von
|
{{ state.progressStatusCount.SUCCESS || 0 }} von
|
||||||
{{
|
{{
|
||||||
(state.progressStatusCount.success || 0) +
|
(state.progressStatusCount.SUCCESS || 0) +
|
||||||
(state.progressStatusCount.unknown || 0)
|
(state.progressStatusCount.UNKNOWN || 0)
|
||||||
}}
|
}}
|
||||||
Lernenden haben ihre Ergebnisse eingereicht.
|
Lernenden haben ihre Ergebnisse eingereicht.
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -174,7 +174,7 @@ function setActiveClasses(translationKey: string) {
|
||||||
class="mr-2 inline-block h-8 w-8"
|
class="mr-2 inline-block h-8 w-8"
|
||||||
></it-icon-smiley-thinking>
|
></it-icon-smiley-thinking>
|
||||||
<p class="text-bold inline-block">
|
<p class="text-bold inline-block">
|
||||||
{{ userCountStatusForCircle(csu.user_id, circle).fail }}
|
{{ userCountStatusForCircle(csu.user_id, circle).FAIL }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<li class="mr-6 flex flex-row items-center">
|
<li class="mr-6 flex flex-row items-center">
|
||||||
|
|
@ -182,7 +182,7 @@ function setActiveClasses(translationKey: string) {
|
||||||
class="mr-2 inline-block h-8 w-8"
|
class="mr-2 inline-block h-8 w-8"
|
||||||
></it-icon-smiley-happy>
|
></it-icon-smiley-happy>
|
||||||
<p class="text-bold inline-block">
|
<p class="text-bold inline-block">
|
||||||
{{ userCountStatusForCircle(csu.user_id, circle).success }}
|
{{ userCountStatusForCircle(csu.user_id, circle).SUCCESS }}
|
||||||
</p>
|
</p>
|
||||||
</li>
|
</li>
|
||||||
<li class="flex flex-row items-center">
|
<li class="flex flex-row items-center">
|
||||||
|
|
@ -190,7 +190,7 @@ function setActiveClasses(translationKey: string) {
|
||||||
class="mr-2 inline-block h-8 w-8"
|
class="mr-2 inline-block h-8 w-8"
|
||||||
></it-icon-smiley-neutral>
|
></it-icon-smiley-neutral>
|
||||||
<p class="text-bold inline-block">
|
<p class="text-bold inline-block">
|
||||||
{{ userCountStatusForCircle(csu.user_id, circle).unknown }}
|
{{ userCountStatusForCircle(csu.user_id, circle).UNKNOWN }}
|
||||||
</p>
|
</p>
|
||||||
</li>
|
</li>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ const failedCriteria = computed(() => {
|
||||||
return competenceStore
|
return competenceStore
|
||||||
.flatPerformanceCriteria()
|
.flatPerformanceCriteria()
|
||||||
.filter((criteria) => {
|
.filter((criteria) => {
|
||||||
return criteria.completion_status === "fail";
|
return criteria.completion_status === "FAIL";
|
||||||
})
|
})
|
||||||
.slice(0, 3);
|
.slice(0, 3);
|
||||||
});
|
});
|
||||||
|
|
@ -111,7 +111,7 @@ const countStatus = computed(() => {
|
||||||
<h5 class="mb-4 text-gray-700">«{{ $t("selfEvaluation.no") }}»</h5>
|
<h5 class="mb-4 text-gray-700">«{{ $t("selfEvaluation.no") }}»</h5>
|
||||||
<div class="flex flex-row items-center">
|
<div class="flex flex-row items-center">
|
||||||
<it-icon-smiley-thinking class="h-16 w-16"></it-icon-smiley-thinking>
|
<it-icon-smiley-thinking class="h-16 w-16"></it-icon-smiley-thinking>
|
||||||
<p class="ml-4 inline-block text-7xl font-bold">{{ countStatus.fail }}</p>
|
<p class="ml-4 inline-block text-7xl font-bold">{{ countStatus.FAIL }}</p>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
<li
|
<li
|
||||||
|
|
@ -121,7 +121,7 @@ const countStatus = computed(() => {
|
||||||
<div class="flex flex-row items-center">
|
<div class="flex flex-row items-center">
|
||||||
<it-icon-smiley-happy class="h-16 w-16"></it-icon-smiley-happy>
|
<it-icon-smiley-happy class="h-16 w-16"></it-icon-smiley-happy>
|
||||||
<p class="ml-4 inline-block text-7xl font-bold">
|
<p class="ml-4 inline-block text-7xl font-bold">
|
||||||
{{ countStatus.success }}
|
{{ countStatus.SUCCESS }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
|
|
@ -130,7 +130,7 @@ const countStatus = computed(() => {
|
||||||
<div class="flex flex-row items-center">
|
<div class="flex flex-row items-center">
|
||||||
<it-icon-smiley-neutral class="h-16 w-16"></it-icon-smiley-neutral>
|
<it-icon-smiley-neutral class="h-16 w-16"></it-icon-smiley-neutral>
|
||||||
<p class="ml-4 inline-block text-7xl font-bold">
|
<p class="ml-4 inline-block text-7xl font-bold">
|
||||||
{{ countStatus.unknown }}
|
{{ countStatus.UNKNOWN }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
|
|
|
||||||
|
|
@ -32,17 +32,17 @@ const { t } = useTranslation();
|
||||||
|
|
||||||
const mobileMenuItems: MenuItem[] = [
|
const mobileMenuItems: MenuItem[] = [
|
||||||
{
|
{
|
||||||
id: "fail",
|
id: "FAIL",
|
||||||
name: `«${t("selfEvaluation.no")}»`,
|
name: `«${t("selfEvaluation.no")}»`,
|
||||||
iconName: "it-icon-smiley-thinking",
|
iconName: "it-icon-smiley-thinking",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "success",
|
id: "SUCCESS",
|
||||||
name: `«${t("selfEvaluation.yes")}»`,
|
name: `«${t("selfEvaluation.yes")}»`,
|
||||||
iconName: "it-icon-smiley-happy",
|
iconName: "it-icon-smiley-happy",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "unknown",
|
id: "UNKNOWN",
|
||||||
name: t("competences.notAssessed"),
|
name: t("competences.notAssessed"),
|
||||||
iconName: "it-icon-smiley-neutral",
|
iconName: "it-icon-smiley-neutral",
|
||||||
},
|
},
|
||||||
|
|
@ -91,7 +91,7 @@ function updateActiveState(status: CourseCompletionStatus) {
|
||||||
:key="item.id"
|
:key="item.id"
|
||||||
:class="{
|
:class="{
|
||||||
'bg-gray-200': activeMenuItem.id === item.id,
|
'bg-gray-200': activeMenuItem.id === item.id,
|
||||||
'mr-6': item.id !== 'unknown',
|
'mr-6': item.id !== 'UNKNOWN',
|
||||||
}"
|
}"
|
||||||
class="mr-6 inline-block px-2 py-4"
|
class="mr-6 inline-block px-2 py-4"
|
||||||
@click="updateActiveState(item.id)"
|
@click="updateActiveState(item.id)"
|
||||||
|
|
|
||||||
|
|
@ -39,11 +39,11 @@ const singleCriteria = computed(() => {
|
||||||
<button
|
<button
|
||||||
class="inline-flex flex-1 items-center border p-4 text-left"
|
class="inline-flex flex-1 items-center border p-4 text-left"
|
||||||
:class="{
|
:class="{
|
||||||
'border-green-500': singleCriteria.completion_status === 'success',
|
'border-green-500': singleCriteria.completion_status === 'SUCCESS',
|
||||||
'border-2': singleCriteria.completion_status === 'success',
|
'border-2': singleCriteria.completion_status === 'SUCCESS',
|
||||||
}"
|
}"
|
||||||
data-cy="success"
|
data-cy="success"
|
||||||
@click="circleStore.markCompletion(singleCriteria, 'success')"
|
@click="circleStore.markCompletion(singleCriteria, 'SUCCESS')"
|
||||||
>
|
>
|
||||||
<it-icon-smiley-happy class="mr-4 h-16 w-16"></it-icon-smiley-happy>
|
<it-icon-smiley-happy class="mr-4 h-16 w-16"></it-icon-smiley-happy>
|
||||||
<span class="text-large font-bold">{{ $t("selfEvaluation.yes") }}</span>
|
<span class="text-large font-bold">{{ $t("selfEvaluation.yes") }}</span>
|
||||||
|
|
@ -51,11 +51,11 @@ const singleCriteria = computed(() => {
|
||||||
<button
|
<button
|
||||||
class="inline-flex flex-1 items-center border p-4 text-left"
|
class="inline-flex flex-1 items-center border p-4 text-left"
|
||||||
:class="{
|
:class="{
|
||||||
'border-orange-500': singleCriteria.completion_status === 'fail',
|
'border-orange-500': singleCriteria.completion_status === 'FAIL',
|
||||||
'border-2': singleCriteria.completion_status === 'fail',
|
'border-2': singleCriteria.completion_status === 'FAIL',
|
||||||
}"
|
}"
|
||||||
data-cy="fail"
|
data-cy="fail"
|
||||||
@click="circleStore.markCompletion(singleCriteria, 'fail')"
|
@click="circleStore.markCompletion(singleCriteria, 'FAIL')"
|
||||||
>
|
>
|
||||||
<it-icon-smiley-thinking class="mr-4 h-16 w-16"></it-icon-smiley-thinking>
|
<it-icon-smiley-thinking class="mr-4 h-16 w-16"></it-icon-smiley-thinking>
|
||||||
<span class="text-xl font-bold">{{ $t("selfEvaluation.no") }}</span>
|
<span class="text-xl font-bold">{{ $t("selfEvaluation.no") }}</span>
|
||||||
|
|
|
||||||
|
|
@ -24,9 +24,9 @@ const props = withDefaults(defineProps<Props>(), {
|
||||||
const circleStore = useCircleStore();
|
const circleStore = useCircleStore();
|
||||||
|
|
||||||
function toggleCompleted(learningContent: LearningContentInterface) {
|
function toggleCompleted(learningContent: LearningContentInterface) {
|
||||||
let completionStatus: CourseCompletionStatus = "success";
|
let completionStatus: CourseCompletionStatus = "SUCCESS";
|
||||||
if (learningContent.completion_status === "success") {
|
if (learningContent.completion_status === "SUCCESS") {
|
||||||
completionStatus = "fail";
|
completionStatus = "FAIL";
|
||||||
}
|
}
|
||||||
circleStore.markCompletion(learningContent, completionStatus);
|
circleStore.markCompletion(learningContent, completionStatus);
|
||||||
}
|
}
|
||||||
|
|
@ -56,7 +56,7 @@ const continueTranslationKeyTuple = computed(() => {
|
||||||
const lastFinished = findLast(
|
const lastFinished = findLast(
|
||||||
circleStore.circle.flatLearningContents,
|
circleStore.circle.flatLearningContents,
|
||||||
(learningContent) => {
|
(learningContent) => {
|
||||||
return learningContent.completion_status === "success";
|
return learningContent.completion_status === "SUCCESS";
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -127,7 +127,7 @@ const learningSequenceBorderClass = computed(() => {
|
||||||
>
|
>
|
||||||
<div v-if="props.readonly">
|
<div v-if="props.readonly">
|
||||||
<it-icon-check
|
<it-icon-check
|
||||||
v-if="learningContent.completion_status === 'success'"
|
v-if="learningContent.completion_status === 'SUCCESS'"
|
||||||
class="block h-8 w-8"
|
class="block h-8 w-8"
|
||||||
></it-icon-check>
|
></it-icon-check>
|
||||||
<div v-else class="h-8 w-8"></div>
|
<div v-else class="h-8 w-8"></div>
|
||||||
|
|
@ -136,8 +136,9 @@ const learningSequenceBorderClass = computed(() => {
|
||||||
v-else
|
v-else
|
||||||
:checkbox-item="{
|
:checkbox-item="{
|
||||||
value: learningContent.completion_status,
|
value: learningContent.completion_status,
|
||||||
checked: learningContent.completion_status === 'success',
|
checked: learningContent.completion_status === 'SUCCESS',
|
||||||
}"
|
}"
|
||||||
|
:disabled="!learningContent.can_user_self_toggle_course_completion"
|
||||||
:data-cy="`${learningContent.slug}-checkbox`"
|
:data-cy="`${learningContent.slug}-checkbox`"
|
||||||
@toggle="toggleCompleted(learningContent)"
|
@toggle="toggleCompleted(learningContent)"
|
||||||
/>
|
/>
|
||||||
|
|
@ -194,14 +195,14 @@ const learningSequenceBorderClass = computed(() => {
|
||||||
@click="!props.readonly && circleStore.openSelfEvaluation(learningUnit)"
|
@click="!props.readonly && circleStore.openSelfEvaluation(learningUnit)"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-if="circleStore.calcSelfEvaluationStatus(learningUnit) === 'success'"
|
v-if="circleStore.calcSelfEvaluationStatus(learningUnit) === 'SUCCESS'"
|
||||||
class="self-evaluation-success flex items-center gap-4 pb-3 lg:pb-6"
|
class="self-evaluation-success flex items-center gap-4 pb-3 lg:pb-6"
|
||||||
>
|
>
|
||||||
<it-icon-smiley-happy class="h-8 w-8 flex-none" data-cy="success" />
|
<it-icon-smiley-happy class="h-8 w-8 flex-none" data-cy="success" />
|
||||||
<div>{{ $t("selfEvaluation.selfEvaluationYes") }}</div>
|
<div>{{ $t("selfEvaluation.selfEvaluationYes") }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-else-if="circleStore.calcSelfEvaluationStatus(learningUnit) === 'fail'"
|
v-else-if="circleStore.calcSelfEvaluationStatus(learningUnit) === 'FAIL'"
|
||||||
class="self-evaluation-fail flex items-center gap-4 pb-3 lg:pb-6"
|
class="self-evaluation-fail flex items-center gap-4 pb-3 lg:pb-6"
|
||||||
>
|
>
|
||||||
<it-icon-smiley-thinking class="h-8 w-8 flex-none" data-cy="fail" />
|
<it-icon-smiley-thinking class="h-8 w-8 flex-none" data-cy="fail" />
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,11 @@ import ItButton from "@/components/ui/ItButton.vue";
|
||||||
import ItCheckbox from "@/components/ui/ItCheckbox.vue";
|
import ItCheckbox from "@/components/ui/ItCheckbox.vue";
|
||||||
import ItSuccessAlert from "@/components/ui/ItSuccessAlert.vue";
|
import ItSuccessAlert from "@/components/ui/ItSuccessAlert.vue";
|
||||||
import { useCurrentCourseSession } from "@/composables";
|
import { useCurrentCourseSession } from "@/composables";
|
||||||
|
import { bustItGetCache } from "@/fetchHelpers";
|
||||||
import { UPSERT_ASSIGNMENT_COMPLETION_MUTATION } from "@/graphql/mutations";
|
import { UPSERT_ASSIGNMENT_COMPLETION_MUTATION } from "@/graphql/mutations";
|
||||||
import AssignmentSubmissionResponses from "@/pages/learningPath/learningContentPage/assignment/AssignmentSubmissionResponses.vue";
|
import AssignmentSubmissionResponses from "@/pages/learningPath/learningContentPage/assignment/AssignmentSubmissionResponses.vue";
|
||||||
import { useCourseSessionsStore } from "@/stores/courseSessions";
|
import { useCourseSessionsStore } from "@/stores/courseSessions";
|
||||||
|
import { useUserStore } from "@/stores/user";
|
||||||
import type { Assignment, AssignmentCompletion, AssignmentTask } from "@/types";
|
import type { Assignment, AssignmentCompletion, AssignmentTask } from "@/types";
|
||||||
import { useMutation } from "@urql/vue";
|
import { useMutation } from "@urql/vue";
|
||||||
import type { Dayjs } from "dayjs";
|
import type { Dayjs } from "dayjs";
|
||||||
|
|
@ -61,7 +63,7 @@ const onEditTask = (task: AssignmentTask) => {
|
||||||
const onSubmit = async () => {
|
const onSubmit = async () => {
|
||||||
try {
|
try {
|
||||||
// noinspection TypeScriptValidateTypes
|
// noinspection TypeScriptValidateTypes
|
||||||
upsertAssignmentCompletionMutation.executeMutation({
|
await upsertAssignmentCompletionMutation.executeMutation({
|
||||||
assignmentId: props.assignment.id.toString(),
|
assignmentId: props.assignment.id.toString(),
|
||||||
courseSessionId: courseSession.value.id.toString(),
|
courseSessionId: courseSession.value.id.toString(),
|
||||||
completionDataString: JSON.stringify({}),
|
completionDataString: JSON.stringify({}),
|
||||||
|
|
@ -70,6 +72,9 @@ const onSubmit = async () => {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
id: props.assignmentCompletion?.id,
|
id: props.assignmentCompletion?.id,
|
||||||
});
|
});
|
||||||
|
bustItGetCache(
|
||||||
|
`/api/course/completion/${courseSession.value.id}/${useUserStore().id}/`
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error("Could not submit assignment", error);
|
log.error("Could not submit assignment", error);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -93,8 +93,8 @@ onUnmounted(() => {
|
||||||
<button
|
<button
|
||||||
class="inline-flex flex-1 items-center border p-4 text-left"
|
class="inline-flex flex-1 items-center border p-4 text-left"
|
||||||
:class="{
|
:class="{
|
||||||
'border-green-500': currentQuestion.completion_status === 'success',
|
'border-green-500': currentQuestion.completion_status === 'SUCCESS',
|
||||||
'border-2': currentQuestion.completion_status === 'success',
|
'border-2': currentQuestion.completion_status === 'SUCCESS',
|
||||||
}"
|
}"
|
||||||
data-cy="success"
|
data-cy="success"
|
||||||
@click="circleStore.markCompletion(currentQuestion, COMPLETION_SUCCESS)"
|
@click="circleStore.markCompletion(currentQuestion, COMPLETION_SUCCESS)"
|
||||||
|
|
@ -112,7 +112,7 @@ onUnmounted(() => {
|
||||||
'border-2': currentQuestion.completion_status === COMPLETION_FAILURE,
|
'border-2': currentQuestion.completion_status === COMPLETION_FAILURE,
|
||||||
}"
|
}"
|
||||||
data-cy="fail"
|
data-cy="fail"
|
||||||
@click="circleStore.markCompletion(currentQuestion, 'fail')"
|
@click="circleStore.markCompletion(currentQuestion, 'FAIL')"
|
||||||
>
|
>
|
||||||
<it-icon-smiley-thinking
|
<it-icon-smiley-thinking
|
||||||
class="mr-4 h-16 w-16"
|
class="mr-4 h-16 w-16"
|
||||||
|
|
|
||||||
|
|
@ -70,13 +70,13 @@ export function calcUserAssignmentCompletionStatus(
|
||||||
if (userAssignmentStatus) {
|
if (userAssignmentStatus) {
|
||||||
userStatus = userAssignmentStatus.completion_status;
|
userStatus = userAssignmentStatus.completion_status;
|
||||||
}
|
}
|
||||||
let progressStatus: StatusCountKey = "unknown";
|
let progressStatus: StatusCountKey = "UNKNOWN";
|
||||||
if (
|
if (
|
||||||
["SUBMITTED", "EVALUATION_IN_PROGRESS", "EVALUATION_SUBMITTED"].includes(
|
["SUBMITTED", "EVALUATION_IN_PROGRESS", "EVALUATION_SUBMITTED"].includes(
|
||||||
userStatus
|
userStatus
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
progressStatus = "success";
|
progressStatus = "SUCCESS";
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -203,7 +203,7 @@ export class Circle implements WagtailCircle {
|
||||||
return (
|
return (
|
||||||
this.flatChildren.filter((lc) => {
|
this.flatChildren.filter((lc) => {
|
||||||
return (
|
return (
|
||||||
lc.completion_status === "success" &&
|
lc.completion_status === "SUCCESS" &&
|
||||||
lc.parentLearningSequence?.translation_key === translationKey
|
lc.parentLearningSequence?.translation_key === translationKey
|
||||||
);
|
);
|
||||||
}).length > 0
|
}).length > 0
|
||||||
|
|
@ -229,12 +229,12 @@ export class Circle implements WagtailCircle {
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
learningContents.every((lc) => lc.completion_status === "success") &&
|
learningContents.every((lc) => lc.completion_status === "SUCCESS") &&
|
||||||
(groupedPerformanceCriteria.length === 0 ||
|
(groupedPerformanceCriteria.length === 0 ||
|
||||||
groupedPerformanceCriteria.every((group) =>
|
groupedPerformanceCriteria.every((group) =>
|
||||||
group.every(
|
group.every(
|
||||||
(pc) =>
|
(pc) =>
|
||||||
pc.completion_status === "success" || pc.completion_status === "fail"
|
pc.completion_status === "SUCCESS" || pc.completion_status === "FAIL"
|
||||||
)
|
)
|
||||||
))
|
))
|
||||||
);
|
);
|
||||||
|
|
@ -252,12 +252,12 @@ export class Circle implements WagtailCircle {
|
||||||
public parseCompletionData(completionData: CourseCompletion[]) {
|
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_id === page.id;
|
||||||
});
|
});
|
||||||
if (pageIndex >= 0) {
|
if (pageIndex >= 0) {
|
||||||
page.completion_status = completionData[pageIndex].completion_status;
|
page.completion_status = completionData[pageIndex].completion_status;
|
||||||
} else {
|
} else {
|
||||||
page.completion_status = "unknown";
|
page.completion_status = "UNKNOWN";
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -25,8 +25,8 @@ function getLastCompleted(courseSlug: string, completionData: CourseCompletion[]
|
||||||
const courseSession = courseSessionsStore.courseSessionForCourse(courseSlug);
|
const courseSession = courseSessionsStore.courseSessionForCourse(courseSlug);
|
||||||
return orderBy(completionData, ["updated_at"], "desc").find((c: CourseCompletion) => {
|
return orderBy(completionData, ["updated_at"], "desc").find((c: CourseCompletion) => {
|
||||||
return (
|
return (
|
||||||
c.completion_status === "success" &&
|
c.completion_status === "SUCCESS" &&
|
||||||
c.course_session === courseSession?.id &&
|
c.course_session_id === courseSession?.id &&
|
||||||
c.page_type.startsWith("learnpath.LearningContent")
|
c.page_type.startsWith("learnpath.LearningContent")
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
@ -129,13 +129,13 @@ export class LearningPath implements WagtailLearningPath {
|
||||||
const lastCircle = this.circles.find((circle) => {
|
const lastCircle = this.circles.find((circle) => {
|
||||||
return circle.flatLearningContents.find(
|
return circle.flatLearningContents.find(
|
||||||
(learningContent) =>
|
(learningContent) =>
|
||||||
learningContent.translation_key === lastCompletedLearningContent.page_key
|
learningContent.id === lastCompletedLearningContent.page_id
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
if (lastCircle) {
|
if (lastCircle) {
|
||||||
const lastLearningContent = lastCircle.flatLearningContents.find(
|
const lastLearningContent = lastCircle.flatLearningContents.find(
|
||||||
(learningContent) =>
|
(learningContent) =>
|
||||||
learningContent.translation_key === lastCompletedLearningContent.page_key
|
learningContent.id === lastCompletedLearningContent.page_id
|
||||||
);
|
);
|
||||||
if (lastLearningContent && lastLearningContent.nextLearningContent) {
|
if (lastLearningContent && lastLearningContent.nextLearningContent) {
|
||||||
this.nextLearningContent = lastLearningContent.nextLearningContent;
|
this.nextLearningContent = lastLearningContent.nextLearningContent;
|
||||||
|
|
|
||||||
|
|
@ -105,7 +105,7 @@ export const useCircleStore = defineStore({
|
||||||
| LearningUnitPerformanceCriteria
|
| LearningUnitPerformanceCriteria
|
||||||
| PerformanceCriteria
|
| PerformanceCriteria
|
||||||
| undefined,
|
| undefined,
|
||||||
completion_status: CourseCompletionStatus = "success"
|
completion_status: CourseCompletionStatus = "SUCCESS"
|
||||||
) {
|
) {
|
||||||
const completionStore = useCompletionStore();
|
const completionStore = useCompletionStore();
|
||||||
|
|
||||||
|
|
@ -146,22 +146,27 @@ export const useCircleStore = defineStore({
|
||||||
},
|
},
|
||||||
calcSelfEvaluationStatus(learningUnit: LearningUnit): CourseCompletionStatus {
|
calcSelfEvaluationStatus(learningUnit: LearningUnit): CourseCompletionStatus {
|
||||||
if (learningUnit.children.length > 0) {
|
if (learningUnit.children.length > 0) {
|
||||||
if (learningUnit.children.every((q) => q.completion_status === "success")) {
|
if (learningUnit.children.every((q) => q.completion_status === "SUCCESS")) {
|
||||||
return "success";
|
return "SUCCESS";
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
learningUnit.children.every(
|
learningUnit.children.every(
|
||||||
(q) => q.completion_status === "fail" || q.completion_status === "success"
|
(q) => q.completion_status === "FAIL" || q.completion_status === "SUCCESS"
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
return "fail";
|
return "FAIL";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return "unknown";
|
return "UNKNOWN";
|
||||||
},
|
},
|
||||||
continueFromLearningContent(currentLearningContent: LearningContentInterface) {
|
continueFromLearningContent(currentLearningContent: LearningContentInterface) {
|
||||||
if (currentLearningContent) {
|
if (currentLearningContent) {
|
||||||
this.markCompletion(currentLearningContent, "success");
|
if (currentLearningContent.can_user_self_toggle_course_completion) {
|
||||||
|
this.markCompletion(currentLearningContent, "SUCCESS");
|
||||||
|
} else {
|
||||||
|
// reload completion data anyway
|
||||||
|
currentLearningContent.parentCircle?.parentLearningPath?.reloadCompletionData();
|
||||||
|
}
|
||||||
this.closeLearningContent(currentLearningContent);
|
this.closeLearningContent(currentLearningContent);
|
||||||
} else {
|
} else {
|
||||||
log.error("currentLearningContent is undefined");
|
log.error("currentLearningContent is undefined");
|
||||||
|
|
|
||||||
|
|
@ -35,16 +35,16 @@ export const useCompetenceStore = defineStore({
|
||||||
if (criteria) {
|
if (criteria) {
|
||||||
const grouped = groupBy(criteria, "completion_status");
|
const grouped = groupBy(criteria, "completion_status");
|
||||||
return {
|
return {
|
||||||
fail: grouped?.fail?.length || 0,
|
UNKNOWN: grouped?.UNKNOWN?.length || 0,
|
||||||
success: grouped?.success?.length || 0,
|
SUCCESS: grouped?.SUCCESS?.length || 0,
|
||||||
unknown: grouped?.unknown?.length || 0,
|
FAIL: grouped?.FAIL?.length || 0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: 0,
|
UNKNOWN: 0,
|
||||||
fail: 0,
|
SUCCESS: 0,
|
||||||
unknown: 0,
|
FAIL: 0,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
criteriaByCompetence(competence: CompetencePage) {
|
criteriaByCompetence(competence: CompetencePage) {
|
||||||
|
|
@ -177,14 +177,14 @@ export const useCompetenceStore = defineStore({
|
||||||
competenceProfilePage.children.forEach((competence) => {
|
competenceProfilePage.children.forEach((competence) => {
|
||||||
competence.children.forEach((performanceCriteria) => {
|
competence.children.forEach((performanceCriteria) => {
|
||||||
const completion = completionData.find(
|
const completion = completionData.find(
|
||||||
(c) => c.page_key === performanceCriteria.translation_key
|
(c) => c.page_id === performanceCriteria.id
|
||||||
);
|
);
|
||||||
if (completion) {
|
if (completion) {
|
||||||
performanceCriteria.completion_status = completion.completion_status;
|
performanceCriteria.completion_status = completion.completion_status;
|
||||||
performanceCriteria.completion_status_updated_at =
|
performanceCriteria.completion_status_updated_at =
|
||||||
completion.updated_at;
|
completion.updated_at;
|
||||||
} else {
|
} else {
|
||||||
performanceCriteria.completion_status = "unknown";
|
performanceCriteria.completion_status = "UNKNOWN";
|
||||||
performanceCriteria.completion_status_updated_at = "";
|
performanceCriteria.completion_status_updated_at = "";
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@ export const useCompletionStore = defineStore({
|
||||||
|
|
||||||
if (courseSessionId) {
|
if (courseSessionId) {
|
||||||
const completionData = await itPost("/api/course/completion/mark/", {
|
const completionData = await itPost("/api/course/completion/mark/", {
|
||||||
page_key: page.translation_key,
|
page_id: page.id,
|
||||||
completion_status: page.completion_status,
|
completion_status: page.completion_status,
|
||||||
course_session_id: courseSessionId,
|
course_session_id: courseSessionId,
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import type { Component } from "vue";
|
||||||
|
|
||||||
export type LoginMethod = "local" | "sso";
|
export type LoginMethod = "local" | "sso";
|
||||||
|
|
||||||
export type CourseCompletionStatus = "unknown" | "fail" | "success";
|
export type CourseCompletionStatus = "UNKNOWN" | "FAIL" | "SUCCESS";
|
||||||
|
|
||||||
export interface BaseCourseWagtailPage {
|
export interface BaseCourseWagtailPage {
|
||||||
readonly id: number;
|
readonly id: number;
|
||||||
|
|
@ -43,6 +43,7 @@ export interface LearningContentInterface extends BaseCourseWagtailPage {
|
||||||
readonly minutes: number;
|
readonly minutes: number;
|
||||||
readonly description: string;
|
readonly description: string;
|
||||||
readonly content_url: string;
|
readonly content_url: string;
|
||||||
|
readonly can_user_self_toggle_course_completion: boolean;
|
||||||
parentCircle: Circle;
|
parentCircle: Circle;
|
||||||
parentLearningSequence?: LearningSequence;
|
parentLearningSequence?: LearningSequence;
|
||||||
parentLearningUnit?: LearningUnit;
|
parentLearningUnit?: LearningUnit;
|
||||||
|
|
@ -169,14 +170,13 @@ export interface Topic extends BaseCourseWagtailPage {
|
||||||
export type LearningPathChild = Topic | WagtailCircle;
|
export type LearningPathChild = Topic | WagtailCircle;
|
||||||
|
|
||||||
export interface CourseCompletion {
|
export interface CourseCompletion {
|
||||||
id: number;
|
readonly id: number;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
user: number;
|
readonly user: number;
|
||||||
page_key: string;
|
readonly page_id: number;
|
||||||
page_type: string;
|
readonly page_type: string;
|
||||||
page_slug: string;
|
readonly course_session_id: number;
|
||||||
course_session: number;
|
|
||||||
completion_status: CourseCompletionStatus;
|
completion_status: CourseCompletionStatus;
|
||||||
additional_json_data: unknown;
|
additional_json_data: unknown;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,8 @@ from vbv_lernwelt.assignment.models import (
|
||||||
)
|
)
|
||||||
from vbv_lernwelt.core.models import User
|
from vbv_lernwelt.core.models import User
|
||||||
from vbv_lernwelt.core.utils import find_first
|
from vbv_lernwelt.core.utils import find_first
|
||||||
from vbv_lernwelt.course.models import CourseSession
|
from vbv_lernwelt.course.models import CourseCompletionStatus, CourseSession
|
||||||
|
from vbv_lernwelt.course.services import mark_course_completion
|
||||||
|
|
||||||
|
|
||||||
def update_assignment_completion(
|
def update_assignment_completion(
|
||||||
|
|
@ -172,6 +173,16 @@ def update_assignment_completion(
|
||||||
acl.completion_data[key].update(task_data)
|
acl.completion_data[key].update(task_data)
|
||||||
acl.save()
|
acl.save()
|
||||||
|
|
||||||
|
if completion_status == AssignmentCompletionStatus.SUBMITTED:
|
||||||
|
learning_content = assignment.learningcontentassignment_set.first()
|
||||||
|
if learning_content:
|
||||||
|
mark_course_completion(
|
||||||
|
user=assignment_user,
|
||||||
|
page=learning_content,
|
||||||
|
course_session=course_session,
|
||||||
|
completion_status=CourseCompletionStatus.SUCCESS.value,
|
||||||
|
)
|
||||||
|
|
||||||
return ac
|
return ac
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
# Generated by Django 3.2.13 on 2023-06-26 15:47
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("competence", "0002_performancecriteria_learning_unit"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="performancecriteria",
|
||||||
|
name="can_user_self_toggle_course_completion",
|
||||||
|
field=models.BooleanField(default=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="performancecriteria",
|
||||||
|
name="has_course_completion_status",
|
||||||
|
field=models.BooleanField(default=True),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -75,6 +75,8 @@ class PerformanceCriteria(CourseBasePage):
|
||||||
blank=True,
|
blank=True,
|
||||||
on_delete=models.SET_NULL,
|
on_delete=models.SET_NULL,
|
||||||
)
|
)
|
||||||
|
has_course_completion_status = models.BooleanField(default=True)
|
||||||
|
can_user_self_toggle_course_completion = models.BooleanField(default=True)
|
||||||
|
|
||||||
content_panels = [
|
content_panels = [
|
||||||
FieldPanel("title"),
|
FieldPanel("title"),
|
||||||
|
|
|
||||||
|
|
@ -3,14 +3,18 @@ import graphene
|
||||||
from vbv_lernwelt.assignment.graphql.mutations import AssignmentMutation
|
from vbv_lernwelt.assignment.graphql.mutations import AssignmentMutation
|
||||||
from vbv_lernwelt.assignment.graphql.queries import AssignmentQuery
|
from vbv_lernwelt.assignment.graphql.queries import AssignmentQuery
|
||||||
from vbv_lernwelt.course.schema import CourseQuery
|
from vbv_lernwelt.course.schema import CourseQuery
|
||||||
|
from vbv_lernwelt.course_session.graphql.mutations import CourseSessionMutation
|
||||||
|
from vbv_lernwelt.course_session.graphql.queries import CourseSessionQuery
|
||||||
from vbv_lernwelt.feedback.graphql.mutations import FeedbackMutation
|
from vbv_lernwelt.feedback.graphql.mutations import FeedbackMutation
|
||||||
|
|
||||||
|
|
||||||
class Query(AssignmentQuery, CourseQuery, graphene.ObjectType):
|
class Query(AssignmentQuery, CourseQuery, CourseSessionQuery, graphene.ObjectType):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class Mutation(AssignmentMutation, FeedbackMutation, graphene.ObjectType):
|
class Mutation(
|
||||||
|
AssignmentMutation, CourseSessionMutation, FeedbackMutation, graphene.ObjectType
|
||||||
|
):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import json
|
import json
|
||||||
import random
|
from datetime import datetime
|
||||||
from datetime import datetime, timedelta
|
|
||||||
|
|
||||||
import wagtail_factories
|
import wagtail_factories
|
||||||
|
from dateutil.relativedelta import relativedelta, TH, TU
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from slugify import slugify
|
from slugify import slugify
|
||||||
|
|
@ -98,6 +98,45 @@ def create_test_course(include_uk=True, include_vv=True, with_sessions=False):
|
||||||
id=TEST_COURSE_SESSION_BERN_ID,
|
id=TEST_COURSE_SESSION_BERN_ID,
|
||||||
start_date=now,
|
start_date=now,
|
||||||
)
|
)
|
||||||
|
csac = CourseSessionAttendanceCourse.objects.create(
|
||||||
|
course_session=cs_bern,
|
||||||
|
learning_content=LearningContentAttendanceCourse.objects.get(
|
||||||
|
slug="test-lehrgang-lp-circle-fahrzeug-lc-präsenzkurs-fahrzeug"
|
||||||
|
),
|
||||||
|
location="Handelsschule KV Bern, Zimmer 123, Eigerstrasse 16, 3012 Bern",
|
||||||
|
trainer="Roland Grossenbacher, roland.grossenbacher@helvetia.ch",
|
||||||
|
)
|
||||||
|
tuesday_in_two_weeks = (
|
||||||
|
datetime.now() + relativedelta(weekday=TU(2)) + relativedelta(weeks=2)
|
||||||
|
)
|
||||||
|
csac.due_date.start = timezone.make_aware(
|
||||||
|
tuesday_in_two_weeks.replace(hour=8, minute=30, second=0, microsecond=0)
|
||||||
|
)
|
||||||
|
csac.due_date.end = timezone.make_aware(
|
||||||
|
tuesday_in_two_weeks.replace(hour=17, minute=0, second=0, microsecond=0)
|
||||||
|
)
|
||||||
|
csac.due_date.save()
|
||||||
|
|
||||||
|
csa = CourseSessionAssignment.objects.create(
|
||||||
|
course_session=cs_bern,
|
||||||
|
learning_content=LearningContentAssignment.objects.get(
|
||||||
|
slug=f"{course.slug}-lp-circle-fahrzeug-lc-überprüfen-einer-motorfahrzeug-versicherungspolice"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
next_thursday = datetime.now() + relativedelta(weekday=TH(2))
|
||||||
|
csa.submission_deadline.start = timezone.make_aware(
|
||||||
|
(next_thursday + relativedelta(weeks=3)).replace(
|
||||||
|
hour=23, minute=59, second=59, microsecond=0
|
||||||
|
)
|
||||||
|
)
|
||||||
|
csa.submission_deadline.save()
|
||||||
|
csa.evaluation_deadline.start = timezone.make_aware(
|
||||||
|
(next_thursday + relativedelta(weeks=5)).replace(
|
||||||
|
hour=23, minute=59, second=59, microsecond=0
|
||||||
|
)
|
||||||
|
)
|
||||||
|
csa.evaluation_deadline.save()
|
||||||
|
|
||||||
cs_zurich = CourseSession.objects.create(
|
cs_zurich = CourseSession.objects.create(
|
||||||
course_id=COURSE_TEST_ID,
|
course_id=COURSE_TEST_ID,
|
||||||
title="Test Zürich 2022 a",
|
title="Test Zürich 2022 a",
|
||||||
|
|
@ -129,18 +168,6 @@ def create_test_course(include_uk=True, include_vv=True, with_sessions=False):
|
||||||
course_session=cs_zurich,
|
course_session=cs_zurich,
|
||||||
user=student2,
|
user=student2,
|
||||||
)
|
)
|
||||||
course = Course.objects.get(id=COURSE_TEST_ID)
|
|
||||||
for cs in CourseSession.objects.filter(course_id=COURSE_TEST_ID):
|
|
||||||
for assignment in LearningContentAssignment.objects.descendant_of(
|
|
||||||
course.coursepage
|
|
||||||
):
|
|
||||||
create_course_session_assignment(cs, assignment)
|
|
||||||
for (
|
|
||||||
attendance_course
|
|
||||||
) in LearningContentAttendanceCourse.objects.descendant_of(
|
|
||||||
course.coursepage
|
|
||||||
):
|
|
||||||
create_course_session_attendance_course(cs, attendance_course)
|
|
||||||
|
|
||||||
return course
|
return course
|
||||||
|
|
||||||
|
|
@ -169,45 +196,6 @@ def create_test_assignment_submitted_data(assignment, course_session, user):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def create_course_session_assignment(course_session, assignment):
|
|
||||||
csa, created = CourseSessionAssignment.objects.get_or_create(
|
|
||||||
course_session=course_session,
|
|
||||||
learning_content=assignment,
|
|
||||||
)
|
|
||||||
|
|
||||||
if course_session.start_date is None:
|
|
||||||
course_session.start_date = datetime.now() + timedelta(days=12)
|
|
||||||
course_session.save()
|
|
||||||
submission_deadline = csa.submission_deadline
|
|
||||||
if submission_deadline:
|
|
||||||
submission_deadline.start = course_session.start_date + timedelta(days=14)
|
|
||||||
submission_deadline.save()
|
|
||||||
evaluation_deadline = csa.evaluation_deadline
|
|
||||||
if evaluation_deadline:
|
|
||||||
evaluation_deadline.start = course_session.start_date + timedelta(days=28)
|
|
||||||
evaluation_deadline.save()
|
|
||||||
return csa
|
|
||||||
|
|
||||||
|
|
||||||
def create_course_session_attendance_course(course_session, course):
|
|
||||||
casc = CourseSessionAttendanceCourse.objects.create(
|
|
||||||
course_session=course_session,
|
|
||||||
learning_content=course,
|
|
||||||
location="Handelsschule KV Bern, Zimmer 123, Eigerstrasse 16, 3012 Bern",
|
|
||||||
trainer="Roland Grossenbacher, roland.grossenbacher@helvetia.ch",
|
|
||||||
)
|
|
||||||
random_week = random.randint(1, 26)
|
|
||||||
|
|
||||||
casc.due_date.start = timezone.make_aware(
|
|
||||||
datetime(2023, 6, 14, 8, 30) + timedelta(weeks=random_week)
|
|
||||||
)
|
|
||||||
casc.due_date.end = timezone.make_aware(
|
|
||||||
datetime(2023, 6, 14, 17, 0) + timedelta(weeks=random_week)
|
|
||||||
)
|
|
||||||
casc.due_date.save()
|
|
||||||
return casc
|
|
||||||
|
|
||||||
|
|
||||||
def create_test_course_with_categories(apps=None, schema_editor=None):
|
def create_test_course_with_categories(apps=None, schema_editor=None):
|
||||||
if apps is not None:
|
if apps is not None:
|
||||||
Course = apps.get_model("course", "Course")
|
Course = apps.get_model("course", "Course")
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import random
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
import djclick as click
|
import djclick as click
|
||||||
|
from dateutil.relativedelta import relativedelta, TH, TU
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
from vbv_lernwelt.assignment.creators.create_assignments import (
|
from vbv_lernwelt.assignment.creators.create_assignments import (
|
||||||
|
|
@ -245,13 +246,6 @@ def create_course_uk_de():
|
||||||
title="Bern 2023 a",
|
title="Bern 2023 a",
|
||||||
)
|
)
|
||||||
|
|
||||||
for i, cs in enumerate(CourseSession.objects.filter(course_id=COURSE_UK_TRAINING)):
|
|
||||||
create_course_session_assignments(
|
|
||||||
cs,
|
|
||||||
f"{course.slug}-lp-circle-fahrzeug-lc-überprüfen-einer-motorfahrzeug-versicherungspolice",
|
|
||||||
i=i,
|
|
||||||
)
|
|
||||||
|
|
||||||
csac = CourseSessionAttendanceCourse.objects.create(
|
csac = CourseSessionAttendanceCourse.objects.create(
|
||||||
course_session=cs,
|
course_session=cs,
|
||||||
learning_content=LearningContentAttendanceCourse.objects.get(
|
learning_content=LearningContentAttendanceCourse.objects.get(
|
||||||
|
|
@ -260,17 +254,37 @@ def create_course_uk_de():
|
||||||
location="Handelsschule KV Bern, Zimmer 123, Eigerstrasse 16, 3012 Bern",
|
location="Handelsschule KV Bern, Zimmer 123, Eigerstrasse 16, 3012 Bern",
|
||||||
trainer="Roland Grossenbacher, roland.grossenbacher@helvetia.ch",
|
trainer="Roland Grossenbacher, roland.grossenbacher@helvetia.ch",
|
||||||
)
|
)
|
||||||
|
tuesday_in_two_weeks = (
|
||||||
# TODO: create dates schlauer
|
datetime.now() + relativedelta(weekday=TU(2)) + relativedelta(weeks=2)
|
||||||
random_week = random.randint(1, 26)
|
)
|
||||||
csac.due_date.start = timezone.make_aware(
|
csac.due_date.start = timezone.make_aware(
|
||||||
datetime(2023, 6, 14, 8, 30) + timedelta(weeks=random_week)
|
tuesday_in_two_weeks.replace(hour=8, minute=30, second=0, microsecond=0)
|
||||||
)
|
)
|
||||||
csac.due_date.end = timezone.make_aware(
|
csac.due_date.end = timezone.make_aware(
|
||||||
datetime(2023, 6, 14, 17, 0) + timedelta(weeks=random_week)
|
tuesday_in_two_weeks.replace(hour=17, minute=0, second=0, microsecond=0)
|
||||||
)
|
)
|
||||||
csac.due_date.save()
|
csac.due_date.save()
|
||||||
|
|
||||||
|
csa = CourseSessionAssignment.objects.create(
|
||||||
|
course_session=cs,
|
||||||
|
learning_content=LearningContentAssignment.objects.get(
|
||||||
|
slug=f"{course.slug}-lp-circle-fahrzeug-lc-überprüfen-einer-motorfahrzeug-versicherungspolice"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
next_thursday = datetime.now() + relativedelta(weekday=TH(2))
|
||||||
|
csa.submission_deadline.start = timezone.make_aware(
|
||||||
|
(next_thursday + relativedelta(weeks=3)).replace(
|
||||||
|
hour=23, minute=59, second=59, microsecond=0
|
||||||
|
)
|
||||||
|
)
|
||||||
|
csa.submission_deadline.save()
|
||||||
|
csa.evaluation_deadline.start = timezone.make_aware(
|
||||||
|
(next_thursday + relativedelta(weeks=5)).replace(
|
||||||
|
hour=23, minute=59, second=59, microsecond=0
|
||||||
|
)
|
||||||
|
)
|
||||||
|
csa.evaluation_deadline.save()
|
||||||
|
|
||||||
# figma demo users and data
|
# figma demo users and data
|
||||||
csu = CourseSessionUser.objects.create(
|
csu = CourseSessionUser.objects.create(
|
||||||
course_session=cs,
|
course_session=cs,
|
||||||
|
|
@ -464,19 +478,19 @@ def create_course_uk_de_completion_data(course_session):
|
||||||
for circle in circles:
|
for circle in circles:
|
||||||
for index, lc in enumerate(circle.get_descendants().type(LearningContent)):
|
for index, lc in enumerate(circle.get_descendants().type(LearningContent)):
|
||||||
mark_course_completion(
|
mark_course_completion(
|
||||||
str(lc.translation_key),
|
page=lc,
|
||||||
User.objects.get(email="lina.egger@example.com"),
|
user=User.objects.get(email="lina.egger@example.com"),
|
||||||
course_session=course_session,
|
course_session=course_session,
|
||||||
completion_status="success",
|
completion_status="SUCCESS",
|
||||||
)
|
)
|
||||||
|
|
||||||
random_number = random.randint(1, 3)
|
random_number = random.randint(1, 3)
|
||||||
if index % random_number == 0:
|
if index % random_number == 0:
|
||||||
mark_course_completion(
|
mark_course_completion(
|
||||||
str(lc.translation_key),
|
page=lc,
|
||||||
User.objects.get(email="michael.meier@example.com"),
|
user=User.objects.get(email="michael.meier@example.com"),
|
||||||
course_session=course_session,
|
course_session=course_session,
|
||||||
completion_status="success",
|
completion_status="SUCCESS",
|
||||||
)
|
)
|
||||||
|
|
||||||
performance_criteria = (
|
performance_criteria = (
|
||||||
|
|
@ -486,26 +500,26 @@ def create_course_uk_de_completion_data(course_session):
|
||||||
)
|
)
|
||||||
for index, pc in enumerate(performance_criteria):
|
for index, pc in enumerate(performance_criteria):
|
||||||
mark_course_completion(
|
mark_course_completion(
|
||||||
str(pc.translation_key),
|
page=pc,
|
||||||
User.objects.get(email="lina.egger@example.com"),
|
user=User.objects.get(email="lina.egger@example.com"),
|
||||||
course_session=course_session,
|
course_session=course_session,
|
||||||
completion_status="success",
|
completion_status="SUCCESS",
|
||||||
)
|
)
|
||||||
|
|
||||||
random_number = random.randint(1, 4)
|
random_number = random.randint(1, 4)
|
||||||
if index % random_number == 0:
|
if index % random_number == 0:
|
||||||
mark_course_completion(
|
mark_course_completion(
|
||||||
str(pc.translation_key),
|
page=pc,
|
||||||
User.objects.get(email="michael.meier@example.com"),
|
user=User.objects.get(email="michael.meier@example.com"),
|
||||||
course_session=course_session,
|
course_session=course_session,
|
||||||
completion_status="success",
|
completion_status="SUCCESS",
|
||||||
)
|
)
|
||||||
if index % random_number == 1:
|
if index % random_number == 1:
|
||||||
mark_course_completion(
|
mark_course_completion(
|
||||||
str(pc.translation_key),
|
page=pc,
|
||||||
User.objects.get(email="michael.meier@example.com"),
|
user=User.objects.get(email="michael.meier@example.com"),
|
||||||
course_session=course_session,
|
course_session=course_session,
|
||||||
completion_status="fail",
|
completion_status="FAIL",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -587,6 +601,7 @@ def create_course_session_assignments(course_session, assignment_slug, i=1):
|
||||||
if course_session.start_date is None:
|
if course_session.start_date is None:
|
||||||
course_session.start_date = datetime.now() + timedelta(days=i * 12)
|
course_session.start_date = datetime.now() + timedelta(days=i * 12)
|
||||||
course_session.save()
|
course_session.save()
|
||||||
|
|
||||||
submission_deadline = csa.submission_deadline
|
submission_deadline = csa.submission_deadline
|
||||||
if submission_deadline:
|
if submission_deadline:
|
||||||
submission_deadline.start = course_session.start_date + timedelta(days=14)
|
submission_deadline.start = course_session.start_date + timedelta(days=14)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,65 @@
|
||||||
|
# Generated by Django 3.2.13 on 2023-06-26 15:24
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
import vbv_lernwelt.course.models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("wagtailcore", "0083_workflowcontenttype"),
|
||||||
|
("course", "0005_remove_coursesession_attendance_courses"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveConstraint(
|
||||||
|
model_name="coursecompletion",
|
||||||
|
name="course_completion_unique_user_page_key",
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name="coursecompletion",
|
||||||
|
name="page_key",
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name="coursecompletion",
|
||||||
|
name="page_slug",
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="coursecompletion",
|
||||||
|
name="page",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
default=1,
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
to="wagtailcore.page",
|
||||||
|
),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="coursecompletion",
|
||||||
|
name="completion_status",
|
||||||
|
field=models.CharField(
|
||||||
|
choices=[
|
||||||
|
(
|
||||||
|
vbv_lernwelt.course.models.CourseCompletionStatus["SUCCESS"],
|
||||||
|
"SUCCESS",
|
||||||
|
),
|
||||||
|
(vbv_lernwelt.course.models.CourseCompletionStatus["FAIL"], "FAIL"),
|
||||||
|
(
|
||||||
|
vbv_lernwelt.course.models.CourseCompletionStatus["UNKNOWN"],
|
||||||
|
"UNKNOWN",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
default="UNKNOWN",
|
||||||
|
max_length=255,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddConstraint(
|
||||||
|
model_name="coursecompletion",
|
||||||
|
constraint=models.UniqueConstraint(
|
||||||
|
fields=("user", "page", "course_session"),
|
||||||
|
name="course_completion_unique_user_page_key",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
# Generated by Django 3.2.13 on 2023-07-11 09:08
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("course", "0006_auto_20230626_1724"),
|
||||||
|
("course", "0006_remove_coursesession_assignment_details_list"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = []
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import enum
|
||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import UniqueConstraint
|
from django.db.models import UniqueConstraint
|
||||||
from django.utils.text import slugify
|
from django.utils.text import slugify
|
||||||
|
|
@ -152,6 +154,12 @@ class CoursePage(CourseBasePage):
|
||||||
return f"{self.title}"
|
return f"{self.title}"
|
||||||
|
|
||||||
|
|
||||||
|
class CourseCompletionStatus(enum.Enum):
|
||||||
|
SUCCESS = "SUCCESS"
|
||||||
|
FAIL = "FAIL"
|
||||||
|
UNKNOWN = "UNKNOWN"
|
||||||
|
|
||||||
|
|
||||||
class CourseCompletion(models.Model):
|
class CourseCompletion(models.Model):
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
updated_at = models.DateTimeField(auto_now=True)
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
|
@ -159,27 +167,24 @@ class CourseCompletion(models.Model):
|
||||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||||
|
|
||||||
# page can logically be a LearningContent or a PerformanceCriteria for now
|
# page can logically be a LearningContent or a PerformanceCriteria for now
|
||||||
page_key = models.UUIDField()
|
page = models.ForeignKey(Page, on_delete=models.CASCADE)
|
||||||
|
|
||||||
|
# store for convenience and performance...
|
||||||
page_type = models.CharField(max_length=255, default="", blank=True)
|
page_type = models.CharField(max_length=255, default="", blank=True)
|
||||||
page_slug = models.CharField(max_length=255, default="", blank=True)
|
|
||||||
|
|
||||||
course_session = models.ForeignKey("course.CourseSession", on_delete=models.CASCADE)
|
course_session = models.ForeignKey("course.CourseSession", on_delete=models.CASCADE)
|
||||||
|
|
||||||
completion_status = models.CharField(
|
completion_status = models.CharField(
|
||||||
max_length=255,
|
max_length=255,
|
||||||
choices=[
|
choices=[(status, status.value) for status in CourseCompletionStatus],
|
||||||
("unknown", "unknown"),
|
default=CourseCompletionStatus.UNKNOWN.value,
|
||||||
("success", "success"),
|
|
||||||
("fail", "fail"),
|
|
||||||
],
|
|
||||||
default="unknown",
|
|
||||||
)
|
)
|
||||||
additional_json_data = models.JSONField(default=dict)
|
additional_json_data = models.JSONField(default=dict)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
constraints = [
|
constraints = [
|
||||||
UniqueConstraint(
|
UniqueConstraint(
|
||||||
fields=["user", "page_key", "course_session"],
|
fields=["user", "page", "course_session"],
|
||||||
name="course_completion_unique_user_page_key",
|
name="course_completion_unique_user_page_key",
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -43,10 +43,9 @@ class CourseCompletionSerializer(serializers.ModelSerializer):
|
||||||
"created_at",
|
"created_at",
|
||||||
"updated_at",
|
"updated_at",
|
||||||
"user",
|
"user",
|
||||||
"page_key",
|
"page_id",
|
||||||
"page_type",
|
"page_type",
|
||||||
"page_slug",
|
"course_session_id",
|
||||||
"course_session",
|
|
||||||
"completion_status",
|
"completion_status",
|
||||||
"additional_json_data",
|
"additional_json_data",
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,31 @@
|
||||||
from wagtail.models import Page
|
from vbv_lernwelt.course.models import CourseCompletion, CourseCompletionStatus
|
||||||
|
|
||||||
from vbv_lernwelt.course.models import CourseCompletion
|
|
||||||
from vbv_lernwelt.learnpath.utils import get_wagtail_type
|
from vbv_lernwelt.learnpath.utils import get_wagtail_type
|
||||||
|
|
||||||
|
|
||||||
def mark_course_completion(page_key, user, course_session, completion_status="success"):
|
def mark_course_completion(
|
||||||
page = Page.objects.get(translation_key=page_key, locale__language_code="de-CH")
|
page, user, course_session, completion_status=CourseCompletionStatus.SUCCESS.value
|
||||||
page_type = get_wagtail_type(page.specific)
|
):
|
||||||
course = page.specific.get_course()
|
if completion_status not in CourseCompletionStatus.__members__:
|
||||||
|
raise ValueError(
|
||||||
|
f"Invalid value for CourseCompletionStatus: {completion_status}"
|
||||||
|
)
|
||||||
|
|
||||||
|
if not (
|
||||||
|
hasattr(page.specific, "has_course_completion_status")
|
||||||
|
and page.specific.has_course_completion_status
|
||||||
|
):
|
||||||
|
return ValueError(
|
||||||
|
f"Page {page.id} of type {get_wagtail_type(page)}"
|
||||||
|
f" cannot be marked as completed"
|
||||||
|
)
|
||||||
|
|
||||||
cc, created = CourseCompletion.objects.get_or_create(
|
cc, created = CourseCompletion.objects.get_or_create(
|
||||||
user=user,
|
user_id=user.id,
|
||||||
page_key=page_key,
|
page_id=page.id,
|
||||||
course_session_id=course_session.id,
|
course_session_id=course_session.id,
|
||||||
)
|
)
|
||||||
cc.page_slug = page.slug
|
|
||||||
cc.page_type = page_type
|
|
||||||
cc.completion_status = completion_status
|
cc.completion_status = completion_status
|
||||||
|
cc.page_type = get_wagtail_type(page.specific)
|
||||||
|
|
||||||
cc.save()
|
cc.save()
|
||||||
return cc
|
return cc
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ class CourseCompletionApiTestCase(APITestCase):
|
||||||
def setUp(self) -> None:
|
def setUp(self) -> None:
|
||||||
create_default_users()
|
create_default_users()
|
||||||
create_test_course(include_uk=False)
|
create_test_course(include_uk=False)
|
||||||
self.user = User.objects.get(username="admin")
|
self.user = User.objects.get(username="test-student1@example.com")
|
||||||
self.cs = CourseSession.objects.create(
|
self.cs = CourseSession.objects.create(
|
||||||
course_id=COURSE_TEST_ID,
|
course_id=COURSE_TEST_ID,
|
||||||
title="Test Lehrgang Session",
|
title="Test Lehrgang Session",
|
||||||
|
|
@ -27,20 +27,19 @@ class CourseCompletionApiTestCase(APITestCase):
|
||||||
course_session=self.cs,
|
course_session=self.cs,
|
||||||
user=self.user,
|
user=self.user,
|
||||||
)
|
)
|
||||||
self.client.login(username="admin", password="test")
|
self.client.force_login(self.user)
|
||||||
|
|
||||||
def test_completeLearningContent_works(self):
|
def test_completeLearningContent_happyCase(self):
|
||||||
learning_content = LearningContentPlaceholder.objects.get(
|
learning_content = LearningContentPlaceholder.objects.get(
|
||||||
title="Fachcheck Reisen"
|
title="Fachcheck Reisen"
|
||||||
)
|
)
|
||||||
learning_content_key = str(learning_content.translation_key)
|
|
||||||
|
|
||||||
mark_url = f"/api/course/completion/mark/"
|
mark_url = f"/api/course/completion/mark/"
|
||||||
|
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
mark_url,
|
mark_url,
|
||||||
{
|
{
|
||||||
"page_key": learning_content_key,
|
"page_id": learning_content.id,
|
||||||
"course_session_id": self.cs.id,
|
"course_session_id": self.cs.id,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
@ -49,13 +48,16 @@ class CourseCompletionApiTestCase(APITestCase):
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertEqual(len(response_json), 1)
|
self.assertEqual(len(response_json), 1)
|
||||||
self.assertEqual(response_json[0]["page_key"], learning_content_key)
|
self.assertEqual(response_json[0]["page_id"], learning_content.id)
|
||||||
self.assertEqual(response_json[0]["completion_status"], "success")
|
self.assertEqual(
|
||||||
|
response_json[0]["page_type"], "learnpath.LearningContentPlaceholder"
|
||||||
|
)
|
||||||
|
self.assertEqual(response_json[0]["completion_status"], "SUCCESS")
|
||||||
|
|
||||||
db_entry = CourseCompletion.objects.get(
|
db_entry = CourseCompletion.objects.get(
|
||||||
user=self.user, course_session_id=self.cs.id, page_key=learning_content_key
|
user=self.user, course_session_id=self.cs.id, page_id=learning_content.id
|
||||||
)
|
)
|
||||||
self.assertEqual(db_entry.completion_status, "success")
|
self.assertEqual(db_entry.completion_status, "SUCCESS")
|
||||||
|
|
||||||
# test getting the circle data
|
# test getting the circle data
|
||||||
response = self.client.get(f"/api/course/completion/{self.cs.id}/")
|
response = self.client.get(f"/api/course/completion/{self.cs.id}/")
|
||||||
|
|
@ -65,15 +67,18 @@ class CourseCompletionApiTestCase(APITestCase):
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertEqual(len(response_json), 1)
|
self.assertEqual(len(response_json), 1)
|
||||||
self.assertEqual(response_json[0]["page_key"], learning_content_key)
|
self.assertEqual(response_json[0]["page_id"], learning_content.id)
|
||||||
self.assertTrue(response_json[0]["completion_status"], "success")
|
self.assertEqual(
|
||||||
|
response_json[0]["page_type"], "learnpath.LearningContentPlaceholder"
|
||||||
|
)
|
||||||
|
self.assertEqual(response_json[0]["completion_status"], "SUCCESS")
|
||||||
|
|
||||||
# test with "fail"
|
# test with "fail"
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
mark_url,
|
mark_url,
|
||||||
{
|
{
|
||||||
"page_key": learning_content_key,
|
"page_id": learning_content.id,
|
||||||
"completion_status": "fail",
|
"completion_status": "FAIL",
|
||||||
"course_session_id": self.cs.id,
|
"course_session_id": self.cs.id,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
@ -81,10 +86,13 @@ class CourseCompletionApiTestCase(APITestCase):
|
||||||
response_json = response.json()
|
response_json = response.json()
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertEqual(len(response_json), 1)
|
self.assertEqual(len(response_json), 1)
|
||||||
self.assertEqual(response_json[0]["page_key"], learning_content_key)
|
self.assertEqual(response_json[0]["page_id"], learning_content.id)
|
||||||
self.assertEqual(response_json[0]["completion_status"], "fail")
|
self.assertEqual(
|
||||||
|
response_json[0]["page_type"], "learnpath.LearningContentPlaceholder"
|
||||||
|
)
|
||||||
|
self.assertEqual(response_json[0]["completion_status"], "FAIL")
|
||||||
|
|
||||||
db_entry = CourseCompletion.objects.get(
|
db_entry = CourseCompletion.objects.get(
|
||||||
user=self.user, course_session_id=self.cs.id, page_key=learning_content_key
|
user=self.user, course_session_id=self.cs.id, page_id=learning_content.id
|
||||||
)
|
)
|
||||||
self.assertEqual(db_entry.completion_status, "fail")
|
self.assertEqual(db_entry.completion_status, "FAIL")
|
||||||
|
|
|
||||||
|
|
@ -89,20 +89,17 @@ def request_course_completion_for_user(request, course_session_id, user_id):
|
||||||
@api_view(["POST"])
|
@api_view(["POST"])
|
||||||
def mark_course_completion_view(request):
|
def mark_course_completion_view(request):
|
||||||
try:
|
try:
|
||||||
page_key = request.data.get("page_key")
|
page_id = request.data.get("page_id")
|
||||||
completion_status = request.data.get("completion_status", "success")
|
completion_status = request.data.get("completion_status", "SUCCESS")
|
||||||
course_session_id = request.data.get("course_session_id")
|
course_session_id = request.data.get("course_session_id")
|
||||||
page = Page.objects.get(translation_key=page_key, locale__language_code="de-CH")
|
page = Page.objects.get(id=page_id)
|
||||||
|
|
||||||
if not has_course_access_by_page_request(request, page):
|
if not has_course_access_by_page_request(request, page):
|
||||||
raise PermissionDenied()
|
raise PermissionDenied()
|
||||||
|
|
||||||
page_type = get_wagtail_type(page.specific)
|
|
||||||
course = page.specific.get_course()
|
|
||||||
|
|
||||||
mark_course_completion(
|
mark_course_completion(
|
||||||
page_key,
|
page=page,
|
||||||
request.user,
|
user=request.user,
|
||||||
course_session=CourseSession.objects.get(id=course_session_id),
|
course_session=CourseSession.objects.get(id=course_session_id),
|
||||||
completion_status=completion_status,
|
completion_status=completion_status,
|
||||||
)
|
)
|
||||||
|
|
@ -117,8 +114,8 @@ def mark_course_completion_view(request):
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"mark_course_completion successful",
|
"mark_course_completion successful",
|
||||||
label="completion_api",
|
label="completion_api",
|
||||||
page_key=page_key,
|
page_id=page_id,
|
||||||
page_type=page_type,
|
page_type=get_wagtail_type(page.specific),
|
||||||
page_slug=page.slug,
|
page_slug=page.slug,
|
||||||
page_title=page.title,
|
page_title=page.title,
|
||||||
user_id=request.user.id,
|
user_id=request.user.id,
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,12 @@ from vbv_lernwelt.course_session.models import (
|
||||||
@admin.register(CourseSessionAttendanceCourse)
|
@admin.register(CourseSessionAttendanceCourse)
|
||||||
class CourseSessionAttendanceCourseAdmin(admin.ModelAdmin):
|
class CourseSessionAttendanceCourseAdmin(admin.ModelAdmin):
|
||||||
# Inline fields are not possible for the DueDate model, because it is not a ForeignKey relatoion.
|
# Inline fields are not possible for the DueDate model, because it is not a ForeignKey relatoion.
|
||||||
readonly_fields = ["course_session", "learning_content", "due_date"]
|
readonly_fields = [
|
||||||
|
"course_session",
|
||||||
|
"learning_content",
|
||||||
|
"due_date",
|
||||||
|
"attendance_user_list",
|
||||||
|
]
|
||||||
list_display = [
|
list_display = [
|
||||||
"course_session",
|
"course_session",
|
||||||
"learning_content",
|
"learning_content",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
import graphene
|
||||||
|
import structlog
|
||||||
|
from rest_framework.exceptions import PermissionDenied
|
||||||
|
|
||||||
|
from vbv_lernwelt.course.permissions import has_course_access
|
||||||
|
from vbv_lernwelt.course_session.graphql.types import CourseSessionAttendanceCourseType
|
||||||
|
from vbv_lernwelt.course_session.models import CourseSessionAttendanceCourse
|
||||||
|
from vbv_lernwelt.course_session.services.attendance import (
|
||||||
|
AttendanceUserStatus,
|
||||||
|
update_attendance_list,
|
||||||
|
)
|
||||||
|
|
||||||
|
logger = structlog.get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class AttendanceUserInputType(graphene.InputObjectType):
|
||||||
|
user_id = graphene.ID(required=True)
|
||||||
|
status = graphene.Field(
|
||||||
|
graphene.Enum.from_enum(AttendanceUserStatus), required=True
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class AttendanceCourseUserMutation(graphene.Mutation):
|
||||||
|
course_session_attendance_course = graphene.Field(CourseSessionAttendanceCourseType)
|
||||||
|
|
||||||
|
class Input:
|
||||||
|
id = graphene.ID(required=True)
|
||||||
|
attendance_user_list = graphene.List(AttendanceUserInputType, required=True)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def mutate(
|
||||||
|
cls,
|
||||||
|
root,
|
||||||
|
info,
|
||||||
|
id,
|
||||||
|
attendance_user_list,
|
||||||
|
):
|
||||||
|
attendance_course = CourseSessionAttendanceCourse.objects.get(id=id)
|
||||||
|
|
||||||
|
if not attendance_course or not has_course_access(
|
||||||
|
info.context.user,
|
||||||
|
attendance_course.course_session.course_id,
|
||||||
|
):
|
||||||
|
raise PermissionDenied()
|
||||||
|
|
||||||
|
attendance_course = update_attendance_list(
|
||||||
|
attendance_course=attendance_course,
|
||||||
|
attendance_user_list=attendance_user_list,
|
||||||
|
)
|
||||||
|
|
||||||
|
return AttendanceCourseUserMutation(
|
||||||
|
course_session_attendance_course=attendance_course
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class CourseSessionMutation:
|
||||||
|
update_course_session_attendance_course_users = AttendanceCourseUserMutation.Field()
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
import graphene
|
||||||
|
from rest_framework.exceptions import PermissionDenied
|
||||||
|
|
||||||
|
from vbv_lernwelt.course.models import CourseSession
|
||||||
|
from vbv_lernwelt.course.permissions import has_course_access, is_course_session_expert
|
||||||
|
from vbv_lernwelt.course_session.graphql.types import CourseSessionAttendanceCourseType
|
||||||
|
from vbv_lernwelt.course_session.models import CourseSessionAttendanceCourse
|
||||||
|
|
||||||
|
|
||||||
|
class CourseSessionQuery(object):
|
||||||
|
course_session_attendance_course = graphene.Field(
|
||||||
|
CourseSessionAttendanceCourseType,
|
||||||
|
id=graphene.ID(required=True),
|
||||||
|
assignment_user_id=graphene.ID(required=False),
|
||||||
|
)
|
||||||
|
|
||||||
|
def resolve_course_session_attendance_course(
|
||||||
|
root,
|
||||||
|
info,
|
||||||
|
id=None,
|
||||||
|
user_id=graphene.ID(required=False),
|
||||||
|
):
|
||||||
|
if user_id is None:
|
||||||
|
user_id = info.context.user.id
|
||||||
|
|
||||||
|
attendance_course = CourseSessionAttendanceCourse.objects.filter(
|
||||||
|
id=id,
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if attendance_course is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if str(user_id) == str(info.context.user.id) or is_course_session_expert(
|
||||||
|
info.context.user, attendance_course.course_session_id
|
||||||
|
):
|
||||||
|
course_id = CourseSession.objects.get(
|
||||||
|
id=attendance_course.course_session_id
|
||||||
|
).course_id
|
||||||
|
if has_course_access(info.context.user, course_id):
|
||||||
|
return attendance_course
|
||||||
|
raise PermissionDenied()
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
import graphene
|
||||||
|
from graphene_django import DjangoObjectType
|
||||||
|
|
||||||
|
from vbv_lernwelt.course_session.models import CourseSessionAttendanceCourse
|
||||||
|
from vbv_lernwelt.course_session.services.attendance import AttendanceUserStatus
|
||||||
|
|
||||||
|
|
||||||
|
class AttendanceUserType(graphene.ObjectType):
|
||||||
|
user_id = graphene.ID(required=True)
|
||||||
|
status = graphene.Field(
|
||||||
|
graphene.Enum.from_enum(AttendanceUserStatus), required=True
|
||||||
|
)
|
||||||
|
first_name = graphene.String()
|
||||||
|
last_name = graphene.String()
|
||||||
|
email = graphene.String()
|
||||||
|
|
||||||
|
|
||||||
|
class CourseSessionAttendanceCourseType(DjangoObjectType):
|
||||||
|
course_session_id = graphene.ID(source="course_session_id")
|
||||||
|
learning_content_id = graphene.ID(source="learning_content_id")
|
||||||
|
due_date_id = graphene.ID(source="due_date_id")
|
||||||
|
end = graphene.DateTime()
|
||||||
|
start = graphene.DateTime()
|
||||||
|
attendance_user_list = graphene.List(
|
||||||
|
AttendanceUserType, source="attendance_user_list"
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = CourseSessionAttendanceCourse
|
||||||
|
fields = (
|
||||||
|
"id",
|
||||||
|
"course_session_id",
|
||||||
|
"learning_content_id",
|
||||||
|
"due_date_id",
|
||||||
|
"location",
|
||||||
|
"trainer",
|
||||||
|
"start",
|
||||||
|
"end",
|
||||||
|
)
|
||||||
|
|
||||||
|
def resolve_start(self, info):
|
||||||
|
if self.due_date is None:
|
||||||
|
return None
|
||||||
|
return self.due_date.start
|
||||||
|
|
||||||
|
def resolve_end(self, info):
|
||||||
|
if self.due_date is None:
|
||||||
|
return None
|
||||||
|
return self.due_date.end
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 3.2.13 on 2023-06-23 15:17
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("course_session", "0001_initial"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="coursesessionattendancecourse",
|
||||||
|
name="attendance_user_list",
|
||||||
|
field=models.JSONField(default=list),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
# Generated by Django 3.2.13 on 2023-07-11 09:08
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("course_session", "0002_coursesessionattendancecourse_attendance_user_list"),
|
||||||
|
("course_session", "0003_auto_20230628_1321"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = []
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from django_jsonform.models.fields import JSONField as JSONSchemaField
|
||||||
|
|
||||||
from vbv_lernwelt.assignment.models import AssignmentType
|
from vbv_lernwelt.assignment.models import AssignmentType
|
||||||
from vbv_lernwelt.duedate.models import DueDate
|
from vbv_lernwelt.duedate.models import DueDate
|
||||||
|
|
@ -29,6 +30,28 @@ class CourseSessionAttendanceCourse(models.Model):
|
||||||
location = models.CharField(max_length=255, blank=True, default="")
|
location = models.CharField(max_length=255, blank=True, default="")
|
||||||
trainer = models.CharField(max_length=255, blank=True, default="")
|
trainer = models.CharField(max_length=255, blank=True, default="")
|
||||||
|
|
||||||
|
# because the attendance list is more of a snapshot of the current state
|
||||||
|
# we will store the attendance list as a JSONField
|
||||||
|
# the important field of the list type is "user_id"
|
||||||
|
ATTENDANCE_USER_LIST_SCHEMA = {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"user_id": {
|
||||||
|
"type": "number",
|
||||||
|
"required": True,
|
||||||
|
},
|
||||||
|
"email": {"type": "string"},
|
||||||
|
"first_name": {"type": "string"},
|
||||||
|
"last_name": {"type": "string"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
attendance_user_list = JSONSchemaField(
|
||||||
|
default=list, schema=ATTENDANCE_USER_LIST_SCHEMA
|
||||||
|
)
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
if not self.pk:
|
if not self.pk:
|
||||||
title = ""
|
title = ""
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ class CourseSessionAttendanceCourseSerializer(serializers.ModelSerializer):
|
||||||
model = CourseSessionAttendanceCourse
|
model = CourseSessionAttendanceCourse
|
||||||
fields = [
|
fields = [
|
||||||
"id",
|
"id",
|
||||||
"course_session_id",
|
"course_session",
|
||||||
"learning_content_id",
|
"learning_content_id",
|
||||||
"due_date_id",
|
"due_date_id",
|
||||||
"location",
|
"location",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,73 @@
|
||||||
|
import enum
|
||||||
|
from typing import List, TypedDict
|
||||||
|
|
||||||
|
from vbv_lernwelt.core.models import User
|
||||||
|
from vbv_lernwelt.course.services import mark_course_completion
|
||||||
|
from vbv_lernwelt.course_session.models import CourseSessionAttendanceCourse
|
||||||
|
|
||||||
|
|
||||||
|
class AttendanceUserStatus(enum.Enum):
|
||||||
|
PRESENT = "PRESENT"
|
||||||
|
ABSENT = "ABSENT"
|
||||||
|
|
||||||
|
|
||||||
|
class AttendanceUser(TypedDict):
|
||||||
|
user_id: str
|
||||||
|
status: AttendanceUserStatus
|
||||||
|
|
||||||
|
|
||||||
|
def update_attendance_list(
|
||||||
|
attendance_course: CourseSessionAttendanceCourse,
|
||||||
|
attendance_user_list: List[AttendanceUser],
|
||||||
|
):
|
||||||
|
user_id_set_before = set(
|
||||||
|
[u["user_id"] for u in attendance_course.attendance_user_list]
|
||||||
|
)
|
||||||
|
|
||||||
|
result_user_list = []
|
||||||
|
for attendance_user in attendance_user_list:
|
||||||
|
u = User.objects.filter(id=attendance_user.get("user_id")).first()
|
||||||
|
if u is not None:
|
||||||
|
result_user_list.append(
|
||||||
|
{
|
||||||
|
"user_id": u.id,
|
||||||
|
"status": attendance_user.get(
|
||||||
|
"status", AttendanceUserStatus.PRESENT
|
||||||
|
).value,
|
||||||
|
"email": u.email,
|
||||||
|
"first_name": u.first_name,
|
||||||
|
"last_name": u.last_name,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
completion_status = (
|
||||||
|
"SUCCESS"
|
||||||
|
if attendance_user.get("status").value == "PRESENT"
|
||||||
|
else "FAIL"
|
||||||
|
)
|
||||||
|
mark_course_completion(
|
||||||
|
page=attendance_course.learning_content,
|
||||||
|
user=u,
|
||||||
|
course_session=attendance_course.course_session,
|
||||||
|
completion_status=completion_status,
|
||||||
|
)
|
||||||
|
|
||||||
|
attendance_course.attendance_user_list = result_user_list
|
||||||
|
attendance_course.save()
|
||||||
|
|
||||||
|
user_id_set_after = set(
|
||||||
|
[u["user_id"] for u in attendance_course.attendance_user_list]
|
||||||
|
)
|
||||||
|
|
||||||
|
user_id_set_removed = user_id_set_before - user_id_set_after
|
||||||
|
|
||||||
|
for user_id in user_id_set_removed:
|
||||||
|
u = User.objects.filter(id=user_id).first()
|
||||||
|
if u is not None:
|
||||||
|
mark_course_completion(
|
||||||
|
page=attendance_course.learning_content,
|
||||||
|
user=u,
|
||||||
|
course_session=attendance_course.course_session,
|
||||||
|
completion_status="FAIL",
|
||||||
|
)
|
||||||
|
|
||||||
|
return attendance_course
|
||||||
|
|
@ -0,0 +1,89 @@
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
from vbv_lernwelt.core.create_default_users import create_default_users
|
||||||
|
from vbv_lernwelt.core.models import User
|
||||||
|
from vbv_lernwelt.course.creators.test_course import create_test_course
|
||||||
|
from vbv_lernwelt.course.models import CourseCompletion, CourseSession
|
||||||
|
from vbv_lernwelt.course.services import mark_course_completion
|
||||||
|
from vbv_lernwelt.course_session.models import CourseSessionAttendanceCourse
|
||||||
|
from vbv_lernwelt.course_session.services.attendance import (
|
||||||
|
AttendanceUserStatus,
|
||||||
|
update_attendance_list,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class AttendanceServicesTestCase(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
create_default_users()
|
||||||
|
create_test_course(include_vv=False, with_sessions=True)
|
||||||
|
self.course_session = CourseSession.objects.get(title="Test Bern 2022 a")
|
||||||
|
self.attendance_course = (
|
||||||
|
self.course_session.coursesessionattendancecourse_set.first()
|
||||||
|
)
|
||||||
|
self.trainer = User.objects.get(username="test-trainer1@example.com")
|
||||||
|
self.client.force_login(self.trainer)
|
||||||
|
|
||||||
|
def test_updateAttendanceList_withSingleUserId_findsDetailsAndStoresResult(self):
|
||||||
|
student = User.objects.get(username="test-student1@example.com")
|
||||||
|
|
||||||
|
update_attendance_list(
|
||||||
|
self.attendance_course,
|
||||||
|
[{"user_id": student.id, "status": AttendanceUserStatus.PRESENT}],
|
||||||
|
)
|
||||||
|
|
||||||
|
attendance_course = CourseSessionAttendanceCourse.objects.get(
|
||||||
|
id=self.attendance_course.id
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
attendance_course.attendance_user_list[0]["user_id"], student.id
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
attendance_course.attendance_user_list,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"email": "test-student1@example.com",
|
||||||
|
"status": "PRESENT",
|
||||||
|
"user_id": -21,
|
||||||
|
"last_name": "Student1",
|
||||||
|
"first_name": "Test",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_updateAttendanceList_willUpdateUserCourseCompletion(self):
|
||||||
|
student = User.objects.get(username="test-student1@example.com")
|
||||||
|
update_attendance_list(
|
||||||
|
self.attendance_course,
|
||||||
|
[{"user_id": student.id, "status": AttendanceUserStatus.PRESENT}],
|
||||||
|
)
|
||||||
|
self.assertEqual(CourseCompletion.objects.count(), 1)
|
||||||
|
cc = CourseCompletion.objects.first()
|
||||||
|
self.assertEqual(cc.user, student)
|
||||||
|
self.assertEqual(cc.completion_status, "SUCCESS")
|
||||||
|
self.assertEqual(cc.page_id, self.attendance_course.learning_content.id)
|
||||||
|
|
||||||
|
def test_updateAttendanceList_withRemovedUser_willUpdateUserCourseCompletion(self):
|
||||||
|
student = User.objects.get(username="test-student1@example.com")
|
||||||
|
self.attendance_course.attendance_user_list = [
|
||||||
|
{
|
||||||
|
"email": "test-student1@example.com",
|
||||||
|
"status": "PRESENT",
|
||||||
|
"user_id": "-21",
|
||||||
|
"last_name": "Student1",
|
||||||
|
"first_name": "Test",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
self.attendance_course.save()
|
||||||
|
mark_course_completion(
|
||||||
|
page=self.attendance_course.learning_content,
|
||||||
|
user=student,
|
||||||
|
course_session=self.course_session,
|
||||||
|
completion_status="SUCCESS",
|
||||||
|
)
|
||||||
|
update_attendance_list(self.attendance_course, [])
|
||||||
|
|
||||||
|
self.assertEqual(CourseCompletion.objects.count(), 1)
|
||||||
|
cc = CourseCompletion.objects.first()
|
||||||
|
self.assertEqual(cc.user, student)
|
||||||
|
self.assertEqual(cc.completion_status, "FAIL")
|
||||||
|
self.assertEqual(cc.page_id, self.attendance_course.learning_content.id)
|
||||||
|
|
@ -0,0 +1,96 @@
|
||||||
|
import json
|
||||||
|
|
||||||
|
from graphene_django.utils.testing import GraphQLTestCase
|
||||||
|
|
||||||
|
from vbv_lernwelt.core.create_default_users import create_default_users
|
||||||
|
from vbv_lernwelt.core.models import User
|
||||||
|
from vbv_lernwelt.course.creators.test_course import create_test_course
|
||||||
|
from vbv_lernwelt.course.models import CourseSession
|
||||||
|
|
||||||
|
|
||||||
|
class AttendanceCourseUserMutationTestCase(GraphQLTestCase):
|
||||||
|
GRAPHQL_URL = "/server/graphql/"
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
create_default_users()
|
||||||
|
create_test_course(include_vv=False, with_sessions=True)
|
||||||
|
self.course_session = CourseSession.objects.get(title="Test Bern 2022 a")
|
||||||
|
self.attendance_course = (
|
||||||
|
self.course_session.coursesessionattendancecourse_set.first()
|
||||||
|
)
|
||||||
|
self.trainer = User.objects.get(username="test-trainer1@example.com")
|
||||||
|
self.client.force_login(self.trainer)
|
||||||
|
|
||||||
|
def test_simple_query(self):
|
||||||
|
response = self.query(
|
||||||
|
f"""
|
||||||
|
{{
|
||||||
|
course_session_attendance_course(id:{self.attendance_course.id}) {{
|
||||||
|
id
|
||||||
|
trainer
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
content = json.loads(response.content)
|
||||||
|
|
||||||
|
self.assertResponseNoErrors(response)
|
||||||
|
self.assertEqual(
|
||||||
|
content["data"]["course_session_attendance_course"]["trainer"],
|
||||||
|
"Roland Grossenbacher, roland.grossenbacher@helvetia.ch",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_mutation_add_user_when_present(self):
|
||||||
|
student = User.objects.get(username="test-student1@example.com")
|
||||||
|
|
||||||
|
query = f"""
|
||||||
|
mutation {{
|
||||||
|
update_course_session_attendance_course_users(
|
||||||
|
id:{self.attendance_course.id},
|
||||||
|
attendance_user_list:[
|
||||||
|
{{user_id: {student.id}, status: PRESENT}},
|
||||||
|
{{user_id: "123123123", status: PRESENT}},
|
||||||
|
]
|
||||||
|
) {{
|
||||||
|
course_session_attendance_course {{
|
||||||
|
id
|
||||||
|
attendance_user_list {{
|
||||||
|
user_id
|
||||||
|
first_name
|
||||||
|
last_name
|
||||||
|
email
|
||||||
|
status
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
"""
|
||||||
|
print(query)
|
||||||
|
response = self.query(query)
|
||||||
|
self.assertResponseNoErrors(response)
|
||||||
|
|
||||||
|
data = json.loads(response.content)
|
||||||
|
|
||||||
|
self.maxDiff = None
|
||||||
|
self.assertDictEqual(
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"update_course_session_attendance_course_users": {
|
||||||
|
"course_session_attendance_course": {
|
||||||
|
"id": str(self.attendance_course.id),
|
||||||
|
"attendance_user_list": [
|
||||||
|
{
|
||||||
|
"user_id": str(student.id),
|
||||||
|
"first_name": student.first_name,
|
||||||
|
"last_name": student.last_name,
|
||||||
|
"email": student.email,
|
||||||
|
"status": "PRESENT",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data,
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,112 @@
|
||||||
|
# Generated by Django 3.2.13 on 2023-06-26 15:47
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("learnpath", "0007_learningunit_title_hidden"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="learningcontentassignment",
|
||||||
|
name="can_user_self_toggle_course_completion",
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="learningcontentassignment",
|
||||||
|
name="has_course_completion_status",
|
||||||
|
field=models.BooleanField(default=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="learningcontentattendancecourse",
|
||||||
|
name="can_user_self_toggle_course_completion",
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="learningcontentattendancecourse",
|
||||||
|
name="has_course_completion_status",
|
||||||
|
field=models.BooleanField(default=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="learningcontentdocumentlist",
|
||||||
|
name="can_user_self_toggle_course_completion",
|
||||||
|
field=models.BooleanField(default=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="learningcontentdocumentlist",
|
||||||
|
name="has_course_completion_status",
|
||||||
|
field=models.BooleanField(default=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="learningcontentfeedback",
|
||||||
|
name="can_user_self_toggle_course_completion",
|
||||||
|
field=models.BooleanField(default=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="learningcontentfeedback",
|
||||||
|
name="has_course_completion_status",
|
||||||
|
field=models.BooleanField(default=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="learningcontentlearningmodule",
|
||||||
|
name="can_user_self_toggle_course_completion",
|
||||||
|
field=models.BooleanField(default=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="learningcontentlearningmodule",
|
||||||
|
name="has_course_completion_status",
|
||||||
|
field=models.BooleanField(default=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="learningcontentmedialibrary",
|
||||||
|
name="can_user_self_toggle_course_completion",
|
||||||
|
field=models.BooleanField(default=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="learningcontentmedialibrary",
|
||||||
|
name="has_course_completion_status",
|
||||||
|
field=models.BooleanField(default=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="learningcontentplaceholder",
|
||||||
|
name="can_user_self_toggle_course_completion",
|
||||||
|
field=models.BooleanField(default=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="learningcontentplaceholder",
|
||||||
|
name="has_course_completion_status",
|
||||||
|
field=models.BooleanField(default=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="learningcontentrichtext",
|
||||||
|
name="can_user_self_toggle_course_completion",
|
||||||
|
field=models.BooleanField(default=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="learningcontentrichtext",
|
||||||
|
name="has_course_completion_status",
|
||||||
|
field=models.BooleanField(default=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="learningcontenttest",
|
||||||
|
name="can_user_self_toggle_course_completion",
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="learningcontenttest",
|
||||||
|
name="has_course_completion_status",
|
||||||
|
field=models.BooleanField(default=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="learningcontentvideo",
|
||||||
|
name="can_user_self_toggle_course_completion",
|
||||||
|
field=models.BooleanField(default=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="learningcontentvideo",
|
||||||
|
name="has_course_completion_status",
|
||||||
|
field=models.BooleanField(default=True),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
# Generated by Django 3.2.13 on 2023-06-26 16:30
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("assignment", "0004_assignment_assignment_type"),
|
||||||
|
("learnpath", "0008_auto_20230626_1747"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="learningcontentassignment",
|
||||||
|
name="content_assignment",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.PROTECT, to="assignment.assignment"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
# Generated by Django 3.2.13 on 2023-07-11 09:08
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("learnpath", "0008_alter_learningcontentassignment_content_assignment"),
|
||||||
|
("learnpath", "0009_alter_learningcontentassignment_content_assignment"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = []
|
||||||
|
|
@ -234,17 +234,21 @@ class LearningContent(CourseBasePage):
|
||||||
"minutes",
|
"minutes",
|
||||||
"description",
|
"description",
|
||||||
"content_url",
|
"content_url",
|
||||||
|
"can_user_self_toggle_course_completion",
|
||||||
]
|
]
|
||||||
|
|
||||||
minutes = models.PositiveIntegerField(default=15)
|
minutes = models.PositiveIntegerField(default=15)
|
||||||
description = RichTextField(blank=True)
|
description = RichTextField(blank=True)
|
||||||
content_url = models.TextField(blank=True)
|
content_url = models.TextField(blank=True)
|
||||||
|
has_course_completion_status = models.BooleanField(default=True)
|
||||||
|
can_user_self_toggle_course_completion = models.BooleanField(default=False)
|
||||||
|
|
||||||
content_panels = [
|
content_panels = [
|
||||||
FieldPanel("title", classname="full title"),
|
FieldPanel("title", classname="full title"),
|
||||||
FieldPanel("minutes"),
|
FieldPanel("minutes"),
|
||||||
FieldPanel("content_url"),
|
FieldPanel("content_url"),
|
||||||
FieldPanel("description"),
|
FieldPanel("description"),
|
||||||
|
FieldPanel("can_user_self_toggle_course_completion"),
|
||||||
]
|
]
|
||||||
|
|
||||||
def get_admin_display_title(self):
|
def get_admin_display_title(self):
|
||||||
|
|
@ -290,26 +294,31 @@ class LearningContentAttendanceCourse(LearningContent):
|
||||||
class LearningContentVideo(LearningContent):
|
class LearningContentVideo(LearningContent):
|
||||||
parent_page_types = ["learnpath.Circle"]
|
parent_page_types = ["learnpath.Circle"]
|
||||||
subpage_types = []
|
subpage_types = []
|
||||||
|
can_user_self_toggle_course_completion = models.BooleanField(default=True)
|
||||||
|
|
||||||
|
|
||||||
class LearningContentPlaceholder(LearningContent):
|
class LearningContentPlaceholder(LearningContent):
|
||||||
parent_page_types = ["learnpath.Circle"]
|
parent_page_types = ["learnpath.Circle"]
|
||||||
subpage_types = []
|
subpage_types = []
|
||||||
|
can_user_self_toggle_course_completion = models.BooleanField(default=True)
|
||||||
|
|
||||||
|
|
||||||
class LearningContentFeedback(LearningContent):
|
class LearningContentFeedback(LearningContent):
|
||||||
parent_page_types = ["learnpath.Circle"]
|
parent_page_types = ["learnpath.Circle"]
|
||||||
subpage_types = []
|
subpage_types = []
|
||||||
|
can_user_self_toggle_course_completion = models.BooleanField(default=True)
|
||||||
|
|
||||||
|
|
||||||
class LearningContentLearningModule(LearningContent):
|
class LearningContentLearningModule(LearningContent):
|
||||||
parent_page_types = ["learnpath.Circle"]
|
parent_page_types = ["learnpath.Circle"]
|
||||||
subpage_types = []
|
subpage_types = []
|
||||||
|
can_user_self_toggle_course_completion = models.BooleanField(default=True)
|
||||||
|
|
||||||
|
|
||||||
class LearningContentMediaLibrary(LearningContent):
|
class LearningContentMediaLibrary(LearningContent):
|
||||||
parent_page_types = ["learnpath.Circle"]
|
parent_page_types = ["learnpath.Circle"]
|
||||||
subpage_types = []
|
subpage_types = []
|
||||||
|
can_user_self_toggle_course_completion = models.BooleanField(default=True)
|
||||||
|
|
||||||
|
|
||||||
class LearningContentTest(LearningContent):
|
class LearningContentTest(LearningContent):
|
||||||
|
|
@ -328,6 +337,7 @@ class LearningContentTest(LearningContent):
|
||||||
|
|
||||||
class LearningContentRichText(LearningContent):
|
class LearningContentRichText(LearningContent):
|
||||||
text = RichTextField(blank=True, features=DEFAULT_RICH_TEXT_FEATURES_WITH_HEADER)
|
text = RichTextField(blank=True, features=DEFAULT_RICH_TEXT_FEATURES_WITH_HEADER)
|
||||||
|
can_user_self_toggle_course_completion = models.BooleanField(default=True)
|
||||||
|
|
||||||
parent_page_types = ["learnpath.Circle"]
|
parent_page_types = ["learnpath.Circle"]
|
||||||
serialize_field_names = LearningContent.serialize_field_names + [
|
serialize_field_names = LearningContent.serialize_field_names + [
|
||||||
|
|
@ -368,6 +378,8 @@ class LearningContentAssignment(LearningContent):
|
||||||
|
|
||||||
|
|
||||||
class LearningContentDocumentList(LearningContent):
|
class LearningContentDocumentList(LearningContent):
|
||||||
|
can_user_self_toggle_course_completion = models.BooleanField(default=True)
|
||||||
|
|
||||||
serialize_field_names = LearningContent.serialize_field_names + [
|
serialize_field_names = LearningContent.serialize_field_names + [
|
||||||
"documents",
|
"documents",
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,12 @@ from vbv_lernwelt.learnpath.models import LearningUnit
|
||||||
class LearningUnitSerializer(
|
class LearningUnitSerializer(
|
||||||
get_course_serializer_class(
|
get_course_serializer_class(
|
||||||
LearningUnit,
|
LearningUnit,
|
||||||
field_names=["evaluate_url", "course_category", "children", "title_hidden"],
|
field_names=[
|
||||||
|
"evaluate_url",
|
||||||
|
"course_category",
|
||||||
|
"children",
|
||||||
|
"title_hidden",
|
||||||
|
],
|
||||||
)
|
)
|
||||||
):
|
):
|
||||||
evaluate_url = SerializerMethodField()
|
evaluate_url = SerializerMethodField()
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue