diff --git a/client/src/pages/cockpit/cockpitPage/AssignmentSubmissionProgress.vue b/client/src/components/cockpit/AssignmentSubmissionProgress.vue
similarity index 100%
rename from client/src/pages/cockpit/cockpitPage/AssignmentSubmissionProgress.vue
rename to client/src/components/cockpit/AssignmentSubmissionProgress.vue
diff --git a/client/src/pages/cockpit/cockpitPage/CockpitDates.vue b/client/src/components/cockpit/CockpitDates.vue
similarity index 100%
rename from client/src/pages/cockpit/cockpitPage/CockpitDates.vue
rename to client/src/components/cockpit/CockpitDates.vue
diff --git a/client/src/pages/cockpit/cockpitPage/SubmissionsOverview.vue b/client/src/components/cockpit/SubmissionsOverview.vue
similarity index 96%
rename from client/src/pages/cockpit/cockpitPage/SubmissionsOverview.vue
rename to client/src/components/cockpit/SubmissionsOverview.vue
index 4c4ea0ac..7dc42662 100644
--- a/client/src/pages/cockpit/cockpitPage/SubmissionsOverview.vue
+++ b/client/src/components/cockpit/SubmissionsOverview.vue
@@ -1,6 +1,6 @@
2
+
+
+
+
diff --git a/client/src/pages/learningPath/selfEvaluationPage/SelfEvaluation.vue b/client/src/components/learningPath/SelfEvaluation.vue
similarity index 74%
rename from client/src/pages/learningPath/selfEvaluationPage/SelfEvaluation.vue
rename to client/src/components/learningPath/SelfEvaluation.vue
index 4a5b8c9a..a0f46a5f 100644
--- a/client/src/pages/learningPath/selfEvaluationPage/SelfEvaluation.vue
+++ b/client/src/components/learningPath/SelfEvaluation.vue
@@ -3,7 +3,7 @@ import { useCircleStore } from "@/stores/circle";
import type { CircleType, LearningUnit } from "@/types";
import * as log from "loglevel";
-import { useCurrentCourseSession, useCourseDataWithCompletion } from "@/composables";
+import { useCourseDataWithCompletion, useCurrentCourseSession } from "@/composables";
import LearningContentContainer from "@/pages/learningPath/learningContentPage/LearningContentContainer.vue";
import LearningContentMultiLayout from "@/pages/learningPath/learningContentPage/layouts/LearningContentMultiLayout.vue";
import eventBus from "@/utils/eventBus";
@@ -11,35 +11,56 @@ import { useRouteQuery } from "@vueuse/router";
import { computed, onUnmounted } from "vue";
import { getPreviousRoute } from "@/router/history";
import { getCompetenceNaviUrl } from "@/utils/utils";
+import SelfEvaluationRequestFeedbackPage from "@/pages/learningPath/selfEvaluationPage/SelfEvaluationRequestFeedbackPage.vue";
log.debug("LearningContent.vue setup");
-const circleStore = useCircleStore();
-const courseSession = useCurrentCourseSession();
-const courseCompletionData = useCourseDataWithCompletion();
-
-const questionIndex = useRouteQuery("step", "0", { transform: Number, mode: "push" });
-const previousRoute = getPreviousRoute();
-
const props = defineProps<{
learningUnit: LearningUnit;
circle: CircleType;
}>();
+const circleStore = useCircleStore();
+const courseSession = useCurrentCourseSession();
+const courseCompletionData = useCourseDataWithCompletion();
+
const questions = computed(() => props.learningUnit?.performance_criteria ?? []);
+const numPages = computed(() => {
+ if (learningUnitHasFeedbackPage.value) {
+ return questions.value.length + 1;
+ } else {
+ return questions.value.length;
+ }
+});
+
+const questionIndex = useRouteQuery("step", "0", { transform: Number, mode: "push" });
+const previousRoute = getPreviousRoute();
+
+const learningUnitHasFeedbackPage = computed(
+ () => props.learningUnit?.feedback_user !== "NO_FEEDBACK"
+);
+
const currentQuestion = computed(() => questions.value[questionIndex.value]);
const showPreviousButton = computed(() => questionIndex.value != 0);
+
const showNextButton = computed(
- () => questionIndex.value + 1 < questions.value?.length && questions.value?.length > 1
+ () => questionIndex.value + 1 < numPages.value && numPages.value > 1
);
-const showExitButton = computed(
- () =>
- questions.value?.length === 1 || questions.value?.length === questionIndex.value + 1
+
+const isLastStep = computed(
+ () => questions.value?.length === 1 || numPages.value == questionIndex.value + 1
);
function handleContinue() {
log.debug("handleContinue");
- if (questionIndex.value + 1 < questions.value.length) {
+
+ // not answering a question is allowed especially,
+ // nonetheless we want to still know this state in the backend!
+ if (currentQuestion.value && currentQuestion.value.completion_status === "UNKNOWN") {
+ courseCompletionData.markCompletion(currentQuestion.value, "UNKNOWN");
+ }
+
+ if (questionIndex.value + 1 < numPages.value) {
log.debug("increment questionIndex", questionIndex.value);
questionIndex.value += 1;
} else {
@@ -50,7 +71,7 @@ function handleContinue() {
function handleBack() {
log.debug("handleBack");
- if (questionIndex.value > 0 && questionIndex.value < questions.value.length) {
+ if (questionIndex.value > 0 && questionIndex.value < numPages.value) {
questionIndex.value -= 1;
}
}
@@ -78,16 +99,20 @@ onUnmounted(() => {
:sub-title="$t('a.Selbsteinschätzung')"
:title="`${learningUnit.title}`"
icon="it-icon-lc-learning-module"
- :steps-count="questions.length"
+ :steps-count="numPages"
:show-next-button="showNextButton"
- :show-exit-button="showExitButton"
+ :show-exit-button="isLastStep"
:show-start-button="false"
:show-previous-button="showPreviousButton"
:base-url="props.learningUnit.evaluate_url"
+ :close-button-variant="learningUnitHasFeedbackPage ? 'close' : 'mark_as_done'"
+ :end-badge-text="
+ learningUnitHasFeedbackPage ? $t('general.submission') : undefined
+ "
@previous="handleBack()"
@next="handleContinue()"
>
-
+
{{ currentQuestion.title }}
@@ -137,6 +162,11 @@ onUnmounted(() => {
+
diff --git a/client/src/components/learningPath/assignment/PraxisAssignmentSubmit.vue b/client/src/components/learningPath/assignment/PraxisAssignmentSubmit.vue
index b1c31c88..fe8bbd27 100644
--- a/client/src/components/learningPath/assignment/PraxisAssignmentSubmit.vue
+++ b/client/src/components/learningPath/assignment/PraxisAssignmentSubmit.vue
@@ -2,7 +2,7 @@
import ItButton from "@/components/ui/ItButton.vue";
import ItCheckbox from "@/components/ui/ItCheckbox.vue";
import { ref } from "vue";
-import { bustItGetCache, useCSRFFetch } from "@/fetchHelpers";
+import { bustItGetCache } from "@/fetchHelpers";
import { useUserStore } from "@/stores/user";
import eventBus from "@/utils/eventBus";
import log from "loglevel";
@@ -12,9 +12,8 @@ import { useMutation } from "@urql/vue";
import { UPSERT_ASSIGNMENT_COMPLETION_MUTATION } from "@/graphql/mutations";
import type { Assignment } from "@/types";
import DateEmbedding from "@/components/dueDates/DateEmbedding.vue";
-import { useCurrentCourseSession } from "@/composables";
-
-const currentCourseSession = useCurrentCourseSession();
+import { useLearningMentors } from "@/composables";
+import NoMentorInformationPanel from "@/components/mentor/NoMentorInformationPanel.vue";
const props = defineProps<{
submissionDeadlineStart?: string | null;
@@ -29,10 +28,7 @@ const upsertAssignmentCompletionMutation = useMutation(
UPSERT_ASSIGNMENT_COMPLETION_MUTATION
);
-const { data: learningMentors } = useCSRFFetch(
- `/api/mentor/${props.courseSessionId}/mentors`
-).json();
-
+const learningMentors = useLearningMentors().learningMentors;
const selectedLearningMentor = ref();
const onSubmit = async () => {
@@ -85,27 +81,7 @@ const onSubmit = async () => {
-
-
-
-
- {{
- $t(
- "a.Aktuell hast du noch keine Person als Lernbegleitung eingeladen. Lade jetzt jemanden ein."
- )
- }}
-
-
- {{ $t("a.Lernbegleitung einladen") }}
-
-
-
+
diff --git a/client/src/components/mentor/NoMentorInformationPanel.vue b/client/src/components/mentor/NoMentorInformationPanel.vue
new file mode 100644
index 00000000..d6170bc3
--- /dev/null
+++ b/client/src/components/mentor/NoMentorInformationPanel.vue
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+ {{
+ $t(
+ "a.Aktuell hast du noch keine Person als Lernbegleitung eingeladen. Lade jetzt jemanden ein."
+ )
+ }}
+
+
+ {{ $t("a.Lernbegleitung einladen") }}
+
+
+
+
diff --git a/client/src/components/selfEvaluationFeedback/FeedbackProvided.vue b/client/src/components/selfEvaluationFeedback/FeedbackProvided.vue
new file mode 100644
index 00000000..f8346fed
--- /dev/null
+++ b/client/src/components/selfEvaluationFeedback/FeedbackProvided.vue
@@ -0,0 +1,79 @@
+
+
+
+
+
{{ criteria.title }}
+
+
+
+
+ {{
+ $t("a.Selbsteinschätzung von FEEDBACK_REQUESTER_NAME", {
+ FEEDBACK_REQUESTER_NAME: feedbackRequesterName,
+ })
+ }}
+
+
+
+
+
+ {{ getSelfEvaluationCaption(criteria.self_assessment) }}
+
+
+
+
+ {{ $t("a.Deine Fremdeinschätzung") }}
+
+
+
+
+
+ {{
+ getFeedbackEvaluationCaption(
+ criteria.feedback_assessment,
+ feedback.feedback_requester_user
+ )
+ }}
+
+
+
+ {{ $t("a.Bearbeiten") }}
+
+
+
+
+
+
+
diff --git a/client/src/components/selfEvaluationFeedback/FeedbackProviderRankCriteria.vue b/client/src/components/selfEvaluationFeedback/FeedbackProviderRankCriteria.vue
new file mode 100644
index 00000000..6374b4a0
--- /dev/null
+++ b/client/src/components/selfEvaluationFeedback/FeedbackProviderRankCriteria.vue
@@ -0,0 +1,67 @@
+
+
+
+
+
{{ $t("a.Leistungsziel") }}:
+
{{ description }}
+
+
+
+ {{ $t("a.Kann FULLNAME das?", { FULLNAME: fullname }) }}
+
+
+
+
+
+
+
+ {{ getFeedbackEvaluationCaption("SUCCESS", requester) }}
+
+
+
+
+
+ {{ getFeedbackEvaluationCaption("FAIL", requester) }}
+
+
+
+
+
+
diff --git a/client/src/components/selfEvaluationFeedback/FeedbackProviderReleaseOverview.vue b/client/src/components/selfEvaluationFeedback/FeedbackProviderReleaseOverview.vue
new file mode 100644
index 00000000..96698ef3
--- /dev/null
+++ b/client/src/components/selfEvaluationFeedback/FeedbackProviderReleaseOverview.vue
@@ -0,0 +1,41 @@
+
+
+
+
+
{{ $t("a.Fremdeinschätzung freigeben") }}
+
+
+ {{
+ $t(
+ "a.Überprüfe deine Eingaben unten und gib anschliessend deine Fremdeinschätzung für FEEDBACK_REQUESTER frei",
+ {
+ FEEDBACK_REQUESTER: feedback.feedback_requester_user.first_name,
+ }
+ )
+ }}
+
+
+ {{ $t("a.Fremdeinschätzung freigeben") }}
+
+
+
+
+
+ {{ $t("a.Du hast deine Fremdeinschätzung freigegeben") }}
+
+
+
+
+
+
+
diff --git a/client/src/components/selfEvaluationFeedback/FeedbackReceived.vue b/client/src/components/selfEvaluationFeedback/FeedbackReceived.vue
new file mode 100644
index 00000000..eedffca1
--- /dev/null
+++ b/client/src/components/selfEvaluationFeedback/FeedbackReceived.vue
@@ -0,0 +1,59 @@
+
+
+
+
+
{{ criteria.title }}
+
+
+
+ {{ $t("a.Deine Selbsteinschätzung") }}
+
+
+
+ {{ getSelfEvaluationCaption(criteria.self_assessment) }}
+
+
+
+
+
+ {{
+ $t("a.Fremdeinschätzung von FEEDBACK_PROVIDER_NAME", {
+ FEEDBACK_PROVIDER_NAME: feedbackProviderName,
+ })
+ }}
+
+
+
+
+
+ {{ getFeedbackReceivedCaption(criteria.feedback_assessment) }}
+
+
+
+
+
+
diff --git a/client/src/components/selfEvaluationFeedback/FeedbackRequested.vue b/client/src/components/selfEvaluationFeedback/FeedbackRequested.vue
new file mode 100644
index 00000000..0f3028f5
--- /dev/null
+++ b/client/src/components/selfEvaluationFeedback/FeedbackRequested.vue
@@ -0,0 +1,51 @@
+
+
+
+
+
+ {{ completion.title }}
+
+ {{ $t("a.Bearbeiten") }}
+
+
+
+
+ {{ $t("selfEvaluation.yes") }}
+
+
+
+ {{ $t("selfEvaluation.no") }}
+
+
+
+ {{ $t("a.Nicht bewertet") }}
+
+
+
+
+
diff --git a/client/src/components/selfEvaluationFeedback/FeedbackRequestedInformationPanel.vue b/client/src/components/selfEvaluationFeedback/FeedbackRequestedInformationPanel.vue
new file mode 100644
index 00000000..9baf746d
--- /dev/null
+++ b/client/src/components/selfEvaluationFeedback/FeedbackRequestedInformationPanel.vue
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+ {{
+ $t("a.Du hast deine Selbsteinschätzung erfolgreich mit FULL_NAME geteilt.", {
+ FULL_NAME: feedbackMentorName,
+ })
+ }}
+
+
+
+ {{
+ $t(
+ "a.FULL_NAME wird eine Fremdeinschätzung für dich vornehmen. Du wirst per Benachrichtigung informiert, sobald die Fremdeinschätzung für dich freigegeben wurde.",
+ { FULL_NAME: feedbackMentorName }
+ )
+ }}
+
+
diff --git a/client/src/components/ui/ItDropdownSelect.vue b/client/src/components/ui/ItDropdownSelect.vue
index 08f522ea..c8bf8462 100644
--- a/client/src/components/ui/ItDropdownSelect.vue
+++ b/client/src/components/ui/ItDropdownSelect.vue
@@ -1,7 +1,7 @@
+
+
+
+
+
+ {{ $t("a.Selbsteinschätzung") }}: {{ selfEvaluationFeedback.title }}
+
+
+ Circle «{{
+ mentorCockpitStore.getCircleTitleById(selfEvaluationFeedback.circle_id)
+ }}»
+
+
+
+
+ {{ selfEvaluationFeedback.pending_evaluations }}
+
+
{{ $t("a.Selbsteinschätzungen geteilt") }}
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ getParticipantById(item.user_id)?.first_name }}
+ {{ getParticipantById(item.user_id)?.last_name }}
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ $t("a.Selbsteinschätzung geteilt") }}
+
+
+
+
+
+
+
+ {{ $t("a.Fremdeinschätzung freigeben") }}
+
+
+
+
+
+ {{ $t("a.Fremdeinschätzung vornehmen") }}
+
+
+ {{ $t("a.Selbsteinschätzung anzeigen") }}
+
+
+
+
+
+
+
diff --git a/client/src/pages/cockpit/cockpitPage/mentor/SelfEvaluationFeedbackPage.vue b/client/src/pages/cockpit/cockpitPage/mentor/SelfEvaluationFeedbackPage.vue
new file mode 100644
index 00000000..e58125a2
--- /dev/null
+++ b/client/src/pages/cockpit/cockpitPage/mentor/SelfEvaluationFeedbackPage.vue
@@ -0,0 +1,161 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/client/src/pages/learningPath/learningContentPage/LearningContentContainer.vue b/client/src/pages/learningPath/learningContentPage/LearningContentContainer.vue
index 83004cf4..194a9864 100644
--- a/client/src/pages/learningPath/learningContentPage/LearningContentContainer.vue
+++ b/client/src/pages/learningPath/learningContentPage/LearningContentContainer.vue
@@ -12,7 +12,7 @@ defineEmits(["exit"]);
-
+
import * as log from "loglevel";
-import SelfEvaluation from "@/pages/learningPath/selfEvaluationPage/SelfEvaluation.vue";
+import SelfEvaluation from "@/components/learningPath/SelfEvaluation.vue";
import { computed } from "vue";
import { useCourseDataWithCompletion } from "@/composables";
@@ -14,9 +14,9 @@ const props = defineProps<{
}>();
const courseData = useCourseDataWithCompletion(props.courseSlug);
-const learningUnit = computed(() =>
- courseData.findLearningUnit(props.learningUnitSlug, props.circleSlug)
-);
+const learningUnit = computed(() => {
+ return courseData.findLearningUnit(props.learningUnitSlug, props.circleSlug);
+});
const circle = computed(() => {
return courseData.findCircle(props.circleSlug);
});
diff --git a/client/src/pages/learningPath/selfEvaluationPage/SelfEvaluationRequestFeedbackPage.vue b/client/src/pages/learningPath/selfEvaluationPage/SelfEvaluationRequestFeedbackPage.vue
new file mode 100644
index 00000000..737a9878
--- /dev/null
+++ b/client/src/pages/learningPath/selfEvaluationPage/SelfEvaluationRequestFeedbackPage.vue
@@ -0,0 +1,133 @@
+
+
+
+
+
+
+
+
+ {{ $t("a.Selbsteinschätzung teilen") }}
+
+
+
+
+
+ {{
+ $t(
+ "a.Du kannst deine Selbsteinschätzung mit deiner Lernbegleitung teilen, damit sie eine Fremdeinschätzung vornimmt."
+ )
+ }}
+
+
+
+
+ {{ $t("a.Selbsteinschätzung teilen") }}
+
+
+ {{
+ $t("a.Selbsteinschätzung mit MENTOR_NAME teilen", {
+ MENTOR_NAME: currentSessionRequestedMentor?.name,
+ })
+ }}
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/client/src/router/index.ts b/client/src/router/index.ts
index e157b92d..5ffa7819 100644
--- a/client/src/router/index.ts
+++ b/client/src/router/index.ts
@@ -180,7 +180,7 @@ const router = createRouter({
{
path: "",
component: () =>
- import("@/pages/cockpit/cockpitPage/mentor/MentorOverview.vue"),
+ import("@/pages/cockpit/cockpitPage/mentor/MentorOverviewPage.vue"),
name: "mentorCockpitOverview",
meta: {
cockpitType: "mentor",
@@ -189,12 +189,24 @@ const router = createRouter({
{
path: "participants",
component: () =>
- import("@/pages/cockpit/cockpitPage/mentor/MentorParticipants.vue"),
+ import("@/pages/cockpit/cockpitPage/mentor/MentorParticipantsPage.vue"),
name: "mentorCockpitParticipants",
meta: {
cockpitType: "mentor",
},
},
+ {
+ path: "self-evaluation-feedback/:learningUnitId",
+ component: () =>
+ import(
+ "@/pages/cockpit/cockpitPage/mentor/SelfEvaluationFeedbackPage.vue"
+ ),
+ name: "mentorSelfEvaluationFeedback",
+ meta: {
+ cockpitType: "mentor",
+ },
+ props: true,
+ },
{
path: "details",
component: () =>
@@ -207,7 +219,7 @@ const router = createRouter({
path: "praxis-assignments/:praxisAssignmentId",
component: () =>
import(
- "@/pages/cockpit/cockpitPage/mentor/MentorPraxisAssignment.vue"
+ "@/pages/cockpit/cockpitPage/mentor/MentorPraxisAssignmentPage.vue"
),
name: "mentorCockpitPraxisAssignments",
meta: {
@@ -215,6 +227,18 @@ const router = createRouter({
},
props: true,
},
+ {
+ path: "self-evaluation-feedback-assignments/:learningUnitId",
+ component: () =>
+ import(
+ "@/pages/cockpit/cockpitPage/mentor/MentorSelfEvaluationFeedbackAssignmentPage.vue"
+ ),
+ name: "mentorCockpitSelfEvaluationFeedbackAssignments",
+ meta: {
+ cockpitType: "mentor",
+ },
+ props: true,
+ },
],
},
],
diff --git a/client/src/services/mentorCockpit.ts b/client/src/services/mentorCockpit.ts
index 0818ae81..d2578974 100644
--- a/client/src/services/mentorCockpit.ts
+++ b/client/src/services/mentorCockpit.ts
@@ -30,7 +30,7 @@ interface Completion {
url: string;
}
-export interface PraxisAssignment {
+export interface Assignment {
id: string;
title: string;
circle_id: string;
@@ -42,7 +42,7 @@ export interface PraxisAssignment {
interface Summary {
participants: Participant[];
circles: Circle[];
- assignments: PraxisAssignment[];
+ assignments: Assignment[];
}
export const useMentorCockpit = (
@@ -60,7 +60,7 @@ export const useMentorCockpit = (
return "";
};
- const getPraxisAssignmentById = (id: string): PraxisAssignment | null => {
+ const getAssignmentById = (id: string): Assignment | null => {
if (summary.value?.assignments) {
const found = summary.value.assignments.find(
(assignment) => assignment.id === id
@@ -93,7 +93,7 @@ export const useMentorCockpit = (
summary,
error,
getCircleTitleById,
- getPraxisAssignmentById,
fetchData,
+ getAssignmentById,
};
};
diff --git a/client/src/services/selfEvaluationFeedback.ts b/client/src/services/selfEvaluationFeedback.ts
new file mode 100644
index 00000000..784beb63
--- /dev/null
+++ b/client/src/services/selfEvaluationFeedback.ts
@@ -0,0 +1,182 @@
+import { useCSRFFetch } from "@/fetchHelpers";
+import type { User } from "@/types";
+import { toValue } from "@vueuse/core";
+import { t } from "i18next";
+import type { Ref } from "vue";
+import { computed, onMounted, ref } from "vue";
+
+export interface FeedbackRequest {
+ feedback_id: string;
+ learning_unit_id: number;
+ circle_name: string;
+ title: string;
+ // submitted => provider submitted (released) his/her feedback
+ feedback_submitted: boolean;
+ feedback_requester_user: User;
+ feedback_provider_user: User;
+ criteria: Criterion[];
+}
+
+export interface Criterion {
+ course_completion_id: string;
+ title: string;
+ self_assessment: "FAIL" | "SUCCESS" | "UNKNOWN";
+ feedback_assessment: "FAIL" | "SUCCESS" | "UNKNOWN";
+}
+
+/** To keep the backend permissions model simple, we have two endpoints:
+ * 1. /requester/: for the user who requested the feedback
+ * 2. /provider/: for the user who provides the feedback
+ *
+ * Design decision: We generally just re-fetch the whole feedback from the backend
+ * after each action (e.g. request, release, add-assessment) to keep the frontend simple.
+ */
+export function useSelfEvaluationFeedback(
+ learningUnitId: Ref
| string,
+ feedbackRole: "requester" | "provider"
+) {
+ const feedback = ref();
+ const loading = ref(false);
+ const error = ref();
+
+ const url = computed(
+ () => `/api/self-evaluation-feedback/${feedbackRole}/${learningUnitId}/feedback`
+ );
+
+ const fetchFeedback = async () => {
+ error.value = undefined;
+ loading.value = true;
+
+ console.log("Fetching feedback for learning unit", learningUnitId);
+ const { data, statusCode, error: _error } = await useCSRFFetch(url.value).json();
+ loading.value = false;
+
+ if (_error.value) {
+ error.value = _error;
+ feedback.value = undefined;
+ return;
+ }
+
+ if (statusCode.value === 404) {
+ feedback.value = undefined;
+ } else {
+ feedback.value = data.value;
+ }
+ };
+
+ const requestFeedback = async (fromProviderUserId: string) => {
+ if (feedbackRole !== "requester") {
+ console.warn("Cannot request feedback");
+ return;
+ }
+
+ const url = `/api/self-evaluation-feedback/requester/${toValue(
+ learningUnitId
+ )}/feedback/start`;
+ await useCSRFFetch(url).post({
+ feedback_provider_user_id: fromProviderUserId,
+ });
+
+ await fetchFeedback();
+ };
+
+ const addFeedbackAssessment = async (
+ courseCompletionId: string,
+ assessment: "FAIL" | "SUCCESS"
+ ) => {
+ if (feedbackRole !== "provider" || !feedback.value) {
+ console.warn("Cannot add feedback assessment");
+ return;
+ }
+
+ await useCSRFFetch(
+ `/api/self-evaluation-feedback/provider/feedback/${feedback.value.feedback_id}/add-assessment`
+ ).put({
+ course_completion_id: courseCompletionId,
+ feedback_assessment: assessment,
+ });
+
+ await fetchFeedback();
+ };
+
+ const releaseFeedback = async () => {
+ if (feedbackRole !== "provider" || !feedback.value) {
+ console.warn("Cannot release feedback");
+ return;
+ }
+
+ await useCSRFFetch(
+ `/api/self-evaluation-feedback/provider/feedback/${feedback.value.feedback_id}/release`
+ ).put({});
+
+ await fetchFeedback();
+ };
+
+ onMounted(fetchFeedback);
+
+ return {
+ feedback,
+ error,
+ loading,
+ // feedback requester actions
+ requestFeedback,
+ // feedback provider actions
+ addFeedbackAssessment,
+ releaseFeedback,
+ };
+}
+
+export const getSmiley = (assessment: "FAIL" | "SUCCESS" | "UNKNOWN") => {
+ switch (assessment) {
+ case "SUCCESS":
+ return "it-icon-smiley-happy";
+ case "FAIL":
+ return "it-icon-smiley-thinking";
+ default:
+ return "it-icon-smiley-neutral";
+ }
+};
+
+export const getSelfEvaluationCaption = (
+ assessment: "FAIL" | "SUCCESS" | "UNKNOWN"
+) => {
+ switch (assessment) {
+ case "SUCCESS":
+ return t("selfEvaluation.yes");
+ case "FAIL":
+ return t("selfEvaluation.no");
+ case "UNKNOWN":
+ return t("a.Nicht bewertet");
+ }
+};
+
+export const getFeedbackReceivedCaption = (
+ assessment: "FAIL" | "SUCCESS" | "UNKNOWN"
+) => {
+ switch (assessment) {
+ case "SUCCESS":
+ return t("receivedEvaluation.yes");
+ case "FAIL":
+ return t("receivedEvaluation.no");
+ case "UNKNOWN":
+ return t("a.Nicht bewertet");
+ }
+};
+
+export const getFeedbackEvaluationCaption = (
+ assessment: "FAIL" | "SUCCESS" | "UNKNOWN",
+ requester: User
+) => {
+ switch (assessment) {
+ case "SUCCESS":
+ return t("a.Ja, NAME kann das.", {
+ NAME: requester.first_name,
+ });
+ case "FAIL":
+ return t("a.Nein, NAME muss das nochmals anschauen.", {
+ NAME: requester.first_name,
+ });
+ case "UNKNOWN":
+ return t("a.Nicht bewertet");
+ }
+};
diff --git a/client/src/types.ts b/client/src/types.ts
index 519df057..ebde7f0d 100644
--- a/client/src/types.ts
+++ b/client/src/types.ts
@@ -457,6 +457,17 @@ export interface ExpertSessionUser extends CourseSessionUser {
role: "EXPERT";
}
+export interface Mentor {
+ id: number;
+ first_name: string;
+ last_name: string;
+}
+
+export interface LearningMentor {
+ id: number;
+ mentor: Mentor;
+}
+
export type CourseSessionDetail = CourseSessionObjectType;
// document upload
@@ -579,3 +590,16 @@ export interface FeedbackData {
};
feedbackType: FeedbackType;
}
+
+export type User = {
+ id: string;
+ first_name: string;
+ last_name: string;
+ email: string;
+ username: string;
+ avatar_url: string;
+ organisation: string | null;
+ is_superuser: boolean;
+ course_session_experts: any[];
+ language: string;
+};
diff --git a/cypress/e2e/circle.cy.js b/cypress/e2e/circle.cy.js
index de1185a1..98d41b73 100644
--- a/cypress/e2e/circle.cy.js
+++ b/cypress/e2e/circle.cy.js
@@ -9,71 +9,71 @@ describe("circle.cy.js", () => {
});
it("can open circle page", () => {
- cy.get("[data-cy=\"circle-title\"]").should("contain", "Fahrzeug");
+ cy.get('[data-cy="circle-title"]').should("contain", "Fahrzeug");
});
it("can toggle learning content", () => {
- cy.get("[data-cy=\"circle-title\"]").should("contain", "Fahrzeug");
+ cy.get('[data-cy="circle-title"]').should("contain", "Fahrzeug");
cy.get(
- "[data-cy=\"test-lehrgang-lp-circle-fahrzeug-lc-handlungsfeld-fahrzeug-checkbox\"]"
+ '[data-cy="test-lehrgang-lp-circle-fahrzeug-lc-handlungsfeld-fahrzeug-checkbox"]'
).should("have.class", "cy-unchecked");
cy.get(
- "[data-cy=\"test-lehrgang-lp-circle-fahrzeug-lc-handlungsfeld-fahrzeug-checkbox\"]"
+ '[data-cy="test-lehrgang-lp-circle-fahrzeug-lc-handlungsfeld-fahrzeug-checkbox"]'
).click();
cy.get(
- "[data-cy=\"test-lehrgang-lp-circle-fahrzeug-lc-handlungsfeld-fahrzeug-checkbox\"]"
+ '[data-cy="test-lehrgang-lp-circle-fahrzeug-lc-handlungsfeld-fahrzeug-checkbox"]'
).should("have.class", "cy-checked");
// completion data should still be there after reload
cy.reload();
cy.get(
- "[data-cy=\"test-lehrgang-lp-circle-fahrzeug-lc-handlungsfeld-fahrzeug-checkbox\"]"
+ '[data-cy="test-lehrgang-lp-circle-fahrzeug-lc-handlungsfeld-fahrzeug-checkbox"]'
).should("have.class", "cy-checked");
});
it("can open learning contents and complete them by continuing", () => {
cy.get(
- "[data-cy=\"test-lehrgang-lp-circle-fahrzeug-lc-verschaffe-dir-einen-überblick\"]"
+ '[data-cy="test-lehrgang-lp-circle-fahrzeug-lc-verschaffe-dir-einen-überblick"]'
).click();
- cy.get("[data-cy=\"lc-title\"]").should(
+ cy.get('[data-cy="lc-title"]').should(
"contain",
"Verschaffe dir einen Überblick"
);
- cy.get("[data-cy=\"complete-and-continue\"]").click({ force: true });
- cy.get("[data-cy=\"circle-title\"]").should("contain", "Fahrzeug");
+ cy.get('[data-cy="complete-and-continue"]').click({ force: true });
+ cy.get('[data-cy="circle-title"]').should("contain", "Fahrzeug");
- cy.get("[data-cy=\"ls-continue-button\"]").click({ force: true });
- cy.get("[data-cy=\"lc-title\"]").should(
+ cy.get('[data-cy="ls-continue-button"]').click();
+ cy.get('[data-cy="lc-title"]').should(
"contain",
"Handlungsfeld «Fahrzeug»"
);
- cy.get("[data-cy=\"complete-and-continue\"]").click({ force: true });
- cy.get("[data-cy=\"circle-title\"]").should("contain", "Fahrzeug");
+ cy.get('[data-cy="complete-and-continue"]').click({ force: true });
+ cy.get('[data-cy="circle-title"]').should("contain", "Fahrzeug");
cy.get(
- "[data-cy=\"test-lehrgang-lp-circle-fahrzeug-lc-verschaffe-dir-einen-überblick-checkbox\"]"
+ '[data-cy="test-lehrgang-lp-circle-fahrzeug-lc-verschaffe-dir-einen-überblick-checkbox"]'
).should("have.class", "cy-checked");
cy.get(
- "[data-cy=\"test-lehrgang-lp-circle-fahrzeug-lc-handlungsfeld-fahrzeug-checkbox\"]"
+ '[data-cy="test-lehrgang-lp-circle-fahrzeug-lc-handlungsfeld-fahrzeug-checkbox"]'
).should("have.class", "cy-checked");
});
it("continue button works", () => {
- cy.get("[data-cy=\"ls-continue-button\"]").should("contain", "Los geht's");
- cy.get("[data-cy=\"ls-continue-button\"]").click();
+ cy.get('[data-cy="ls-continue-button"]').should("contain", "Los geht's");
+ cy.get('[data-cy="ls-continue-button"]').click();
- cy.get("[data-cy=\"lc-title\"]").should(
+ cy.get('[data-cy="lc-title"]').should(
"contain",
"Verschaffe dir einen Überblick"
);
- cy.get("[data-cy=\"complete-and-continue\"]").click({ force: true });
+ cy.get('[data-cy="complete-and-continue"]').click({ force: true });
- cy.get("[data-cy=\"ls-continue-button\"]").should("contain", "Weiter geht's");
- cy.get("[data-cy=\"ls-continue-button\"]").click();
- cy.get("[data-cy=\"lc-title\"]").should(
+ cy.get('[data-cy="ls-continue-button"]').should("contain", "Weiter geht's");
+ cy.get('[data-cy="ls-continue-button"]').click();
+ cy.get('[data-cy="lc-title"]').should(
"contain",
"Handlungsfeld «Fahrzeug»"
);
@@ -81,43 +81,43 @@ describe("circle.cy.js", () => {
it("can open learning content by url", () => {
cy.visit("/course/test-lehrgang/learn/fahrzeug/handlungsfeld-fahrzeug");
- cy.get("[data-cy=\"lc-title\"]").should(
+ cy.get('[data-cy="lc-title"]').should(
"contain",
"Handlungsfeld «Fahrzeug»"
);
- cy.get("[data-cy=\"close-learning-content\"]").click();
- cy.get("[data-cy=\"circle-title\"]").should("contain", "Fahrzeug");
+ cy.get('[data-cy="close-learning-content"]').click();
+ cy.get('[data-cy="circle-title"]').should("contain", "Fahrzeug");
});
it("checks number of sequences and contents", () => {
- cy.get("[data-cy=\"lp-learning-sequence\"]").should("have.length", 3);
- cy.get("[data-cy=\"lp-learning-sequence\"]")
+ cy.get('[data-cy="lp-learning-sequence"]').should("have.length", 3);
+ cy.get('[data-cy="lp-learning-sequence"]')
.first()
.should("contain", "Vorbereitung");
- cy.get("[data-cy=\"lp-learning-sequence\"]")
+ cy.get('[data-cy="lp-learning-sequence"]')
.eq(1)
.should("contain", "Training");
- cy.get("[data-cy=\"lp-learning-sequence\"]")
+ cy.get('[data-cy="lp-learning-sequence"]')
.last()
.should("contain", "Transfer");
- cy.get("[data-cy=\"lp-learning-content\"]").should("have.length", 10);
- cy.get("[data-cy=\"lp-learning-content\"]")
+ cy.get('[data-cy="lp-learning-content"]').should("have.length", 10);
+ cy.get('[data-cy="lp-learning-content"]')
.first()
.should("contain", "Verschaffe dir einen Überblick");
- cy.get("[data-cy=\"lp-learning-content\"]")
+ cy.get('[data-cy="lp-learning-content"]')
.eq(4)
.should("contain", "Präsenzkurs Fahrzeug");
- cy.get("[data-cy=\"lp-learning-content\"]")
+ cy.get('[data-cy="lp-learning-content"]')
.eq(7)
.should("contain", "Reflexion");
- cy.get("[data-cy=\"lp-learning-content\"]")
+ cy.get('[data-cy="lp-learning-content"]')
.last()
.should("contain", "Feedback");
cy.visit("/course/test-lehrgang/learn/reisen");
- cy.get("[data-cy=\"lp-learning-sequence\"]").should("have.length", 3);
- cy.get("[data-cy=\"lp-learning-content\"]").should("have.length", 9);
+ cy.get('[data-cy="lp-learning-sequence"]').should("have.length", 3);
+ cy.get('[data-cy="lp-learning-content"]').should("have.length", 9);
});
});
diff --git a/server/config/settings/base.py b/server/config/settings/base.py
index b073746e..81090a32 100644
--- a/server/config/settings/base.py
+++ b/server/config/settings/base.py
@@ -133,6 +133,7 @@ LOCAL_APPS = [
"vbv_lernwelt.course_session_group",
"vbv_lernwelt.shop",
"vbv_lernwelt.learning_mentor",
+ "vbv_lernwelt.self_evaluation_feedback",
]
# https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps
INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + LOCAL_APPS
diff --git a/server/config/urls.py b/server/config/urls.py
index 56b9ad07..1f2b36fa 100644
--- a/server/config/urls.py
+++ b/server/config/urls.py
@@ -141,6 +141,9 @@ urlpatterns = [
path("api/mentor//", include("vbv_lernwelt.learning_mentor.urls")),
+ # self evaluation feedback
+ path("api/self-evaluation-feedback/", include("vbv_lernwelt.self_evaluation_feedback.urls")),
+
# assignment
path(
r"api/assignment///status/",
diff --git a/server/vbv_lernwelt/assignment/migrations/0012_auto_20240124_1004.py b/server/vbv_lernwelt/assignment/migrations/0012_auto_20240124_1004.py
new file mode 100644
index 00000000..0489bb0b
--- /dev/null
+++ b/server/vbv_lernwelt/assignment/migrations/0012_auto_20240124_1004.py
@@ -0,0 +1,36 @@
+# Generated by Django 3.2.20 on 2024-01-24 09:04
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("assignment", "0011_assignment_solution_sample"),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name="assignment",
+ name="assignment_type",
+ field=models.CharField(
+ choices=[
+ ("PRAXIS_ASSIGNMENT", "PRAXIS_ASSIGNMENT"),
+ ("CASEWORK", "CASEWORK"),
+ ("PREP_ASSIGNMENT", "PREP_ASSIGNMENT"),
+ ("REFLECTION", "REFLECTION"),
+ ("CONDITION_ACCEPTANCE", "CONDITION_ACCEPTANCE"),
+ ("EDONIQ_TEST", "EDONIQ_TEST"),
+ ],
+ default="CASEWORK",
+ max_length=50,
+ ),
+ ),
+ migrations.AlterField(
+ model_name="assignment",
+ name="needs_expert_evaluation",
+ field=models.BooleanField(
+ default=False,
+ help_text="Muss der Auftrag durch eine/n Experten/in oder eine Lernbegleitung beurteilt werden?",
+ ),
+ ),
+ ]
diff --git a/server/vbv_lernwelt/core/constants.py b/server/vbv_lernwelt/core/constants.py
index 56f0495f..599ff03b 100644
--- a/server/vbv_lernwelt/core/constants.py
+++ b/server/vbv_lernwelt/core/constants.py
@@ -25,6 +25,7 @@ TEST_STUDENT1_USER_ID = "65c73ad0-6d53-43a9-a4a4-64143f27b03a"
TEST_STUDENT2_USER_ID = "19c40d94-15cc-4198-aaad-ef707c4b0900"
TEST_STUDENT3_USER_ID = "bcf94dba-53bc-474b-a22d-e4af39aa042b"
TEST_MENTOR1_USER_ID = "d1f5f5a9-5b0a-4e1a-9e1a-9e9b5b5e1b1b"
+TEST_STUDENT1_VV_USER_ID = "5ff59857-8de5-415e-a387-4449f9a0337a"
TEST_COURSE_SESSION_BERN_ID = -1
TEST_COURSE_SESSION_ZURICH_ID = -2
diff --git a/server/vbv_lernwelt/core/create_default_users.py b/server/vbv_lernwelt/core/create_default_users.py
index a401ba6c..e0560d34 100644
--- a/server/vbv_lernwelt/core/create_default_users.py
+++ b/server/vbv_lernwelt/core/create_default_users.py
@@ -13,6 +13,7 @@ from vbv_lernwelt.core.constants import (
ADMIN_USER_ID,
TEST_MENTOR1_USER_ID,
TEST_STUDENT1_USER_ID,
+ TEST_STUDENT1_VV_USER_ID,
TEST_STUDENT2_USER_ID,
TEST_STUDENT3_USER_ID,
TEST_SUPERVISOR1_USER_ID,
@@ -210,9 +211,10 @@ def create_default_users(default_password="test", set_avatar=False):
last_name="Expert3",
)
_create_student_user(
+ id=TEST_STUDENT1_VV_USER_ID,
email="student-vv@eiger-versicherungen.ch",
- first_name="Student",
- last_name="VV",
+ first_name="Viktor",
+ last_name="Vollgas",
)
_create_student_user(
email="patrizia.huggel@eiger-versicherungen.ch",
@@ -364,10 +366,11 @@ def create_default_users(default_password="test", set_avatar=False):
_create_user(
_id=TEST_MENTOR1_USER_ID,
email="test-mentor1@example.com",
- first_name="[Mentor]",
- last_name="Mentor",
+ first_name="Micheala",
+ last_name="Weber-Mentor",
password=default_password,
language="de",
+ avatar_image="uk1.patrizia.huggel.jpg",
)
diff --git a/server/vbv_lernwelt/core/management/commands/cypress_reset.py b/server/vbv_lernwelt/core/management/commands/cypress_reset.py
index 8c3cd409..d4a660af 100644
--- a/server/vbv_lernwelt/core/management/commands/cypress_reset.py
+++ b/server/vbv_lernwelt/core/management/commands/cypress_reset.py
@@ -13,7 +13,7 @@ from vbv_lernwelt.core.constants import (
TEST_STUDENT3_USER_ID,
TEST_TRAINER1_USER_ID,
)
-from vbv_lernwelt.core.models import User
+from vbv_lernwelt.core.models import Organisation, User
from vbv_lernwelt.course.consts import COURSE_TEST_ID
from vbv_lernwelt.course.creators.test_course import (
create_edoniq_test_result_data,
@@ -108,6 +108,7 @@ def command(
CourseSessionAttendanceCourse.objects.all().update(attendance_user_list=[])
LearningMentor.objects.all().delete()
+ User.objects.all().update(organisation=Organisation.objects.first())
User.objects.all().update(language="de")
User.objects.all().update(additional_json_data={})
@@ -331,16 +332,32 @@ def command(
attendance_course.save()
if create_learning_mentor:
- print("Create learning mentor")
- mentor = LearningMentor.objects.create(
+ uk_mentor = LearningMentor.objects.create(
course=Course.objects.get(id=COURSE_TEST_ID),
mentor=User.objects.get(id=TEST_MENTOR1_USER_ID),
)
- course_session = CourseSession.objects.get(id=TEST_COURSE_SESSION_BERN_ID)
- csu = CourseSessionUser.objects.get(
- user__id=TEST_STUDENT1_USER_ID, course_session=course_session
+ uk_mentor.participants.add(
+ CourseSessionUser.objects.get(
+ user__id=TEST_STUDENT1_USER_ID,
+ course_session=CourseSession.objects.get(
+ id=TEST_COURSE_SESSION_BERN_ID
+ ),
+ )
)
- mentor.participants.add(csu)
+
+ # FIXME: Add mentor to VV course as well, once we can:
+ # -> https://bitbucket.org/iterativ/vbv_lernwelt/pull-requests/287
+ # vv_course = Course.objects.get(id=COURSE_VERSICHERUNGSVERMITTLERIN_ID)
+ # vv_course_session = CourseSession.objects.get(course=vv_course)
+ # vv_mentor = LearningMentor.objects.create(
+ # course=vv_course,
+ # mentor=User.objects.get(id=TEST_MENTOR1_USER_ID),
+ # )
+ # vv_mentor.participants.add(
+ # CourseSessionUser.objects.get(
+ # user__id=TEST_STUDENT1_VV_USER_ID, course_session=vv_course_session
+ # )
+ # )
course = Course.objects.get(id=COURSE_TEST_ID)
course.enable_circle_documents = enable_circle_documents
diff --git a/server/vbv_lernwelt/core/views.py b/server/vbv_lernwelt/core/views.py
index aeb6825e..854530a1 100644
--- a/server/vbv_lernwelt/core/views.py
+++ b/server/vbv_lernwelt/core/views.py
@@ -167,6 +167,10 @@ def cypress_reset_view(request):
request.data.get("create_attendance_days") == "true"
)
+ options["create_learning_mentor"] = (
+ request.data.get("create_learning_mentor") == "true"
+ )
+
call_command(
"cypress_reset",
**options,
diff --git a/server/vbv_lernwelt/course/creators/test_utils.py b/server/vbv_lernwelt/course/creators/test_utils.py
index 3a396754..3f945b97 100644
--- a/server/vbv_lernwelt/course/creators/test_utils.py
+++ b/server/vbv_lernwelt/course/creators/test_utils.py
@@ -46,6 +46,7 @@ from vbv_lernwelt.learnpath.models import (
LearningContentAssignment,
LearningContentEdoniqTest,
LearningPath,
+ LearningUnit,
)
from vbv_lernwelt.learnpath.tests.learning_path_factories import (
CircleFactory,
@@ -58,11 +59,13 @@ from vbv_lernwelt.learnpath.tests.learning_path_factories import (
)
-def create_course(title: str, _id=None) -> Tuple[Course, CoursePage]:
+def create_course(
+ title: str = "Course Title", _id=None, course_page_title: str = "Test Lehrgang"
+) -> Tuple[Course, CoursePage]:
course = Course.objects.create(id=_id, title=title, category_name="Handlungsfeld")
course_page = CoursePageFactory(
- title="Test Lehrgang",
+ title=course_page_title,
parent=get_wagtail_default_site().root_page,
course=course,
)
@@ -268,10 +271,28 @@ def create_course_session_edoniq_test(
return cset
+def create_learning_unit(
+ circle: Circle,
+ course: Course,
+ course_category_title: str = "Course Category",
+) -> LearningUnit:
+ cat, _ = CourseCategory.objects.get_or_create(
+ course=course,
+ title=course_category_title,
+ )
+
+ return LearningUnitFactory(
+ title="Learning Unit",
+ parent=circle,
+ course_category=cat,
+ )
+
+
def create_performance_criteria_page(
course: Course,
course_page: CoursePage,
circle: Circle,
+ learning_unit: LearningUnitFactory | None = None,
) -> PerformanceCriteria:
competence_navi_page = CompetenceNaviPageFactory(
title="Competence Navi",
@@ -290,17 +311,14 @@ def create_performance_criteria_page(
items=[("item", "Action Competence Item")],
)
- cat, _ = CourseCategory.objects.get_or_create(
- course=course, title="Course Category"
- )
-
- lu = LearningUnitFactory(title="Learning Unit", parent=circle, course_category=cat)
+ if not learning_unit:
+ learning_unit = create_learning_unit(circle=circle, course=course)
return PerformanceCriteriaFactory(
parent=action_competence,
competence_id="X1.1",
title="Performance Criteria",
- learning_unit=lu,
+ learning_unit=learning_unit,
)
diff --git a/server/vbv_lernwelt/course_session/migrations/0006_auto_20240124_1004.py b/server/vbv_lernwelt/course_session/migrations/0006_auto_20240124_1004.py
new file mode 100644
index 00000000..9aab3304
--- /dev/null
+++ b/server/vbv_lernwelt/course_session/migrations/0006_auto_20240124_1004.py
@@ -0,0 +1,36 @@
+# Generated by Django 3.2.20 on 2024-01-24 09:04
+
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("duedate", "0008_auto_20231108_0747"),
+ ("course_session", "0005_auto_20230825_1723"),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name="coursesessionassignment",
+ name="evaluation_deadline",
+ field=models.OneToOneField(
+ blank=True,
+ null=True,
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="assignment_evaluation_deadline",
+ to="duedate.duedate",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="coursesessionassignment",
+ name="submission_deadline",
+ field=models.OneToOneField(
+ blank=True,
+ null=True,
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="assignment_submission_deadline",
+ to="duedate.duedate",
+ ),
+ ),
+ ]
diff --git a/server/vbv_lernwelt/duedate/migrations/0005_auto_20230925_1648.py b/server/vbv_lernwelt/duedate/migrations/0005_auto_20230925_1648.py
index 1f3192a8..9dcd192b 100644
--- a/server/vbv_lernwelt/duedate/migrations/0005_auto_20230925_1648.py
+++ b/server/vbv_lernwelt/duedate/migrations/0005_auto_20230925_1648.py
@@ -45,6 +45,7 @@ class Migration(migrations.Migration):
dependencies = [
("duedate", "0004_alter_duedate_start"),
("learnpath", "0008_add_edoniq_sequence_id"),
+ ("course_session", "0005_auto_20230825_1723"),
]
operations = [
diff --git a/server/vbv_lernwelt/learning_mentor/content/praxis_assignment.py b/server/vbv_lernwelt/learning_mentor/content/praxis_assignment.py
index b1746f5f..6e74e3f1 100644
--- a/server/vbv_lernwelt/learning_mentor/content/praxis_assignment.py
+++ b/server/vbv_lernwelt/learning_mentor/content/praxis_assignment.py
@@ -10,9 +10,10 @@ from vbv_lernwelt.core.models import User
from vbv_lernwelt.course.models import CourseSession
from vbv_lernwelt.course_session.models import CourseSessionAssignment
from vbv_lernwelt.learning_mentor.entities import (
- CompletionStatus,
- PraxisAssignmentCompletion,
- PraxisAssignmentStatus,
+ MentorAssignmentCompletion,
+ MentorAssignmentStatus,
+ MentorAssignmentStatusType,
+ MentorCompletionStatus,
)
@@ -21,7 +22,7 @@ def get_assignment_completions(
assignment: Assignment,
participants: List[User],
evaluation_user: User,
-) -> List[PraxisAssignmentCompletion]:
+) -> List[MentorAssignmentCompletion]:
evaluation_results = AssignmentCompletion.objects.filter(
assignment_user__in=participants,
course_session=course_session,
@@ -34,14 +35,14 @@ def get_assignment_completions(
completion_status = result["completion_status"]
if completion_status == AssignmentCompletionStatus.EVALUATION_SUBMITTED.value:
- status = CompletionStatus.EVALUATED
+ status = MentorCompletionStatus.EVALUATED
elif completion_status in [
AssignmentCompletionStatus.SUBMITTED.value,
AssignmentCompletionStatus.EVALUATION_IN_PROGRESS.value,
]:
- status = CompletionStatus.SUBMITTED
+ status = MentorCompletionStatus.SUBMITTED
else:
- status = CompletionStatus.UNKNOWN
+ status = MentorCompletionStatus.UNKNOWN
user_status_map[result["assignment_user"]] = (
status,
@@ -49,25 +50,25 @@ def get_assignment_completions(
)
status_priority = {
- CompletionStatus.SUBMITTED: 1,
- CompletionStatus.EVALUATED: 2,
- CompletionStatus.UNKNOWN: 3,
+ MentorCompletionStatus.SUBMITTED: 1,
+ MentorCompletionStatus.EVALUATED: 2,
+ MentorCompletionStatus.UNKNOWN: 3,
}
sorted_participants = sorted(
participants,
key=lambda u: (
status_priority.get(
- user_status_map.get(u.id, (CompletionStatus.UNKNOWN, ""))[0]
+ user_status_map.get(u.id, (MentorCompletionStatus.UNKNOWN, ""))[0]
),
user_status_map.get(u.id, ("", u.last_name))[1],
),
)
return [
- PraxisAssignmentCompletion(
+ MentorAssignmentCompletion(
status=user_status_map.get(
- user.id, (CompletionStatus.UNKNOWN, user.last_name)
+ user.id, (MentorCompletionStatus.UNKNOWN, user.last_name)
)[0],
user_id=user.id,
last_name=user.last_name,
@@ -79,7 +80,7 @@ def get_assignment_completions(
def get_praxis_assignments(
course_session: CourseSession, participants: List[User], evaluation_user: User
-) -> Tuple[List[PraxisAssignmentStatus], Set[int]]:
+) -> Tuple[List[MentorAssignmentStatus], Set[int]]:
records = []
circle_ids = set()
@@ -105,19 +106,20 @@ def get_praxis_assignments(
[
completion
for completion in completions
- if completion.status == CompletionStatus.SUBMITTED
+ if completion.status == MentorCompletionStatus.SUBMITTED
]
)
circle_id = learning_content.get_circle().id
records.append(
- PraxisAssignmentStatus(
+ MentorAssignmentStatus(
id=course_session_assignment.id,
title=learning_content.content_assignment.title,
circle_id=circle_id,
pending_evaluations=submitted_count,
completions=completions,
+ type=MentorAssignmentStatusType.PRAXIS_ASSIGNMENT,
)
)
diff --git a/server/vbv_lernwelt/learning_mentor/content/self_evaluation_feedback.py b/server/vbv_lernwelt/learning_mentor/content/self_evaluation_feedback.py
new file mode 100644
index 00000000..163cf8c2
--- /dev/null
+++ b/server/vbv_lernwelt/learning_mentor/content/self_evaluation_feedback.py
@@ -0,0 +1,108 @@
+from typing import List, Set, Tuple
+
+import structlog
+
+from vbv_lernwelt.core.models import User
+from vbv_lernwelt.course.models import Course
+from vbv_lernwelt.learning_mentor.entities import (
+ MentorAssignmentCompletion,
+ MentorAssignmentStatus,
+ MentorAssignmentStatusType,
+ MentorCompletionStatus,
+)
+from vbv_lernwelt.learnpath.models import (
+ LearningUnit,
+ LearningUnitPerformanceFeedbackType,
+)
+from vbv_lernwelt.self_evaluation_feedback.models import SelfEvaluationFeedback
+
+logger = structlog.get_logger(__name__)
+
+
+def create_blank_completions_non_requesters(
+ completions: List[MentorAssignmentCompletion],
+ participants: List[User],
+) -> List[MentorAssignmentCompletion]:
+ non_requester_completions = []
+
+ participants_user_ids = set([str(p.id) for p in participants])
+ completion_seen_user_ids = set([str(c.user_id) for c in completions])
+
+ user_by_id = {str(p.id): p for p in participants}
+ for non_requester_user_id in participants_user_ids - completion_seen_user_ids:
+ non_requester_user = user_by_id[non_requester_user_id]
+
+ non_requester_completions.append(
+ MentorAssignmentCompletion(
+ status=MentorCompletionStatus.UNKNOWN,
+ user_id=non_requester_user.id,
+ last_name=non_requester_user.last_name,
+ url="",
+ )
+ )
+
+ return non_requester_completions
+
+
+def get_self_feedback_evaluation(
+ participants: List[User],
+ evaluation_user: User,
+ course: Course,
+) -> Tuple[List[MentorAssignmentStatus], Set[int]]:
+ records: List[MentorAssignmentStatus] = []
+ circle_ids: Set[int] = set()
+
+ if not participants:
+ return records, circle_ids
+
+ # very unfortunate: we can't simply get all SelfEvaluationFeedback objects since then
+ # we would miss the one where no feedback was requested -> so we get all learning units
+ # and check if we have to take them into account (course, feedback type, etc.)
+ for learning_unit in LearningUnit.objects.filter(
+ feedback_user=LearningUnitPerformanceFeedbackType.MENTOR_FEEDBACK.value,
+ course_category__course_id=course.id,
+ ):
+ feedbacks = SelfEvaluationFeedback.objects.filter(
+ learning_unit=learning_unit,
+ feedback_requester_user__in=participants,
+ feedback_provider_user=evaluation_user,
+ )
+
+ circle_id = learning_unit.get_circle().id
+ circle_ids.add(circle_id)
+
+ pending_evaluations = len([f for f in feedbacks if not f.feedback_submitted])
+
+ completions = [
+ MentorAssignmentCompletion(
+ # feedback_submitted as seen from the perspective of the evaluation user (feedback provider)
+ # means that the feedback has been evaluated by the feedback provider, hence the status is EVALUATED
+ status=MentorCompletionStatus.EVALUATED
+ if f.feedback_submitted
+ else MentorCompletionStatus.SUBMITTED,
+ user_id=f.feedback_requester_user.id,
+ last_name=f.feedback_requester_user.last_name,
+ url=f"/course/{course.slug}/cockpit/mentor/self-evaluation-feedback/{f.learning_unit.id}",
+ )
+ for f in feedbacks
+ ]
+
+ # requesting feedback is optional, so we need to add blank completions
+ # for those mentees who did not request a feedback
+ completions += create_blank_completions_non_requesters(
+ completions=completions,
+ participants=participants,
+ )
+
+ records.append(
+ MentorAssignmentStatus(
+ id=learning_unit.id,
+ title=learning_unit.title,
+ circle_id=circle_id,
+ pending_evaluations=pending_evaluations,
+ completions=completions,
+ type=MentorAssignmentStatusType.SELF_EVALUATION_FEEDBACK,
+ )
+ )
+
+ return records, circle_ids
diff --git a/server/vbv_lernwelt/learning_mentor/entities.py b/server/vbv_lernwelt/learning_mentor/entities.py
index 2b4a9a04..d0a93152 100644
--- a/server/vbv_lernwelt/learning_mentor/entities.py
+++ b/server/vbv_lernwelt/learning_mentor/entities.py
@@ -3,24 +3,30 @@ from enum import Enum
from typing import List
-class CompletionStatus(str, Enum):
+class MentorCompletionStatus(str, Enum):
UNKNOWN = "UNKNOWN"
SUBMITTED = "SUBMITTED"
EVALUATED = "EVALUATED"
+class MentorAssignmentStatusType(str, Enum):
+ PRAXIS_ASSIGNMENT = "praxis_assignment"
+ SELF_EVALUATION_FEEDBACK = "self_evaluation_feedback"
+
+
@dataclass
-class PraxisAssignmentCompletion:
- status: CompletionStatus
+class MentorAssignmentCompletion:
+ status: MentorCompletionStatus
user_id: str
last_name: str
url: str
@dataclass
-class PraxisAssignmentStatus:
+class MentorAssignmentStatus:
id: str
title: str
circle_id: str
pending_evaluations: int
- completions: List[PraxisAssignmentCompletion]
+ completions: List[MentorAssignmentCompletion]
+ type: MentorAssignmentStatusType
diff --git a/server/vbv_lernwelt/learning_mentor/serializers.py b/server/vbv_lernwelt/learning_mentor/serializers.py
index 20333aaf..8c079cbe 100644
--- a/server/vbv_lernwelt/learning_mentor/serializers.py
+++ b/server/vbv_lernwelt/learning_mentor/serializers.py
@@ -4,7 +4,7 @@ from vbv_lernwelt.core.serializers import UserSerializer
from vbv_lernwelt.learning_mentor.models import LearningMentor, MentorInvitation
-class PraxisAssignmentCompletionSerializer(serializers.Serializer):
+class MentorAssignmentCompletionSerializer(serializers.Serializer):
status = serializers.SerializerMethodField()
user_id = serializers.CharField()
last_name = serializers.CharField()
@@ -15,13 +15,13 @@ class PraxisAssignmentCompletionSerializer(serializers.Serializer):
return obj.status.value
-class PraxisAssignmentStatusSerializer(serializers.Serializer):
+class MentorAssignmentStatusSerializer(serializers.Serializer):
id = serializers.CharField()
title = serializers.CharField()
circle_id = serializers.CharField()
pending_evaluations = serializers.IntegerField()
- completions = PraxisAssignmentCompletionSerializer(many=True)
- type = serializers.ReadOnlyField(default="praxis_assignment")
+ completions = MentorAssignmentCompletionSerializer(many=True)
+ type = serializers.ReadOnlyField()
class InvitationSerializer(serializers.ModelSerializer):
diff --git a/server/vbv_lernwelt/learning_mentor/tests/test_assignments.py b/server/vbv_lernwelt/learning_mentor/tests/test_assignments.py
index b60d78cc..794e721a 100644
--- a/server/vbv_lernwelt/learning_mentor/tests/test_assignments.py
+++ b/server/vbv_lernwelt/learning_mentor/tests/test_assignments.py
@@ -18,7 +18,7 @@ from vbv_lernwelt.learning_mentor.content.praxis_assignment import (
get_assignment_completions,
get_praxis_assignments,
)
-from vbv_lernwelt.learning_mentor.entities import CompletionStatus
+from vbv_lernwelt.learning_mentor.entities import MentorCompletionStatus
class AttendanceServicesTestCase(TestCase):
@@ -74,10 +74,10 @@ class AttendanceServicesTestCase(TestCase):
# THEN
expected_order = ["Beta", "Alpha", "Gamma", "Kappa"]
expected_statuses = {
- "Alpha": CompletionStatus.EVALUATED, # user1
- "Beta": CompletionStatus.SUBMITTED, # user2
- "Gamma": CompletionStatus.UNKNOWN, # user4 (no AssignmentCompletion)
- "Kappa": CompletionStatus.UNKNOWN, # user3 (IN_PROGRESS should be PENDING)
+ "Alpha": MentorCompletionStatus.EVALUATED, # user1
+ "Beta": MentorCompletionStatus.SUBMITTED, # user2
+ "Gamma": MentorCompletionStatus.UNKNOWN, # user4 (no AssignmentCompletion)
+ "Kappa": MentorCompletionStatus.UNKNOWN, # user3 (IN_PROGRESS should be PENDING)
}
self.assertEqual(len(results), len(participants))
diff --git a/server/vbv_lernwelt/learning_mentor/tests/test_mentor.py b/server/vbv_lernwelt/learning_mentor/tests/test_mentor.py
index d70e6d93..60b280c7 100644
--- a/server/vbv_lernwelt/learning_mentor/tests/test_mentor.py
+++ b/server/vbv_lernwelt/learning_mentor/tests/test_mentor.py
@@ -1,3 +1,5 @@
+from typing import Dict, List, Optional
+
from django.urls import reverse
from rest_framework import status
from rest_framework.test import APITestCase
@@ -7,6 +9,7 @@ from vbv_lernwelt.assignment.models import (
AssignmentCompletionStatus,
AssignmentType,
)
+from vbv_lernwelt.core.admin import User
from vbv_lernwelt.course.creators.test_utils import (
add_course_session_user,
create_assignment,
@@ -15,10 +18,22 @@ from vbv_lernwelt.course.creators.test_utils import (
create_course,
create_course_session,
create_course_session_assignment,
+ create_learning_unit,
create_user,
)
from vbv_lernwelt.course.models import CourseSessionUser
from vbv_lernwelt.learning_mentor.models import LearningMentor
+from vbv_lernwelt.learnpath.models import LearningUnitPerformanceFeedbackType
+from vbv_lernwelt.self_evaluation_feedback.models import SelfEvaluationFeedback
+
+
+def get_completion_for_user(
+ completions: List[Dict[str, str]], user: User
+) -> Optional[Dict[str, str]]:
+ for completion in completions:
+ if completion["user_id"] == str(user.id):
+ return completion
+ return None
class LearningMentorAPITest(APITestCase):
@@ -28,15 +43,6 @@ class LearningMentorAPITest(APITestCase):
self.circle, _ = create_circle(title="Circle", course_page=self.course_page)
- self.assignment = create_assignment(
- course=self.course, assignment_type=AssignmentType.PRAXIS_ASSIGNMENT
- )
-
- lca = create_assignment_learning_content(self.circle, self.assignment)
- create_course_session_assignment(
- course_session=self.course_session, learning_content_assignment=lca
- )
-
self.mentor = create_user("mentor")
self.participant_1 = add_course_session_user(
self.course_session,
@@ -109,7 +115,7 @@ class LearningMentorAPITest(APITestCase):
self.assertEqual(participant_1["first_name"], "Test")
self.assertEqual(participant_1["last_name"], "Participant_1")
- def test_api_praxis_assignments(self) -> None:
+ def test_api_self_evaluation_feedback(self) -> None:
# GIVEN
participants = [self.participant_1, self.participant_2, self.participant_3]
self.client.force_login(self.mentor)
@@ -118,12 +124,104 @@ class LearningMentorAPITest(APITestCase):
mentor=self.mentor,
course=self.course_session.course,
)
+
+ mentor.participants.set(participants)
+
+ learning_unit = create_learning_unit(
+ circle=self.circle,
+ course=self.course,
+ )
+
+ # performance criteria under this learning unit shall be evaluated by the mentor
+ learning_unit.feedback_user = (
+ LearningUnitPerformanceFeedbackType.MENTOR_FEEDBACK.name
+ )
+
+ learning_unit.save()
+
+ # 1: we already evaluated
+ SelfEvaluationFeedback.objects.create(
+ feedback_requester_user=self.participant_1.user,
+ feedback_provider_user=self.mentor,
+ learning_unit=learning_unit,
+ feedback_submitted=True,
+ )
+
+ # 2: we have not evaluated yet
+ SelfEvaluationFeedback.objects.create(
+ feedback_requester_user=self.participant_2.user,
+ feedback_provider_user=self.mentor,
+ learning_unit=learning_unit,
+ feedback_submitted=False,
+ )
+
+ # 3: did not request feedback
+ # ...
+
+ # WHEN
+ response = self.client.get(self.url)
+
+ # THEN
+ assignments = response.data["assignments"]
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+
+ self.assertEqual(
+ response.data["circles"],
+ [{"id": self.circle.id, "title": self.circle.title}],
+ )
+
+ self.assertEqual(len(assignments), 1)
+ assignment = assignments[0]
+
+ self.assertEqual(assignment["type"], "self_evaluation_feedback")
+ self.assertEqual(assignment["pending_evaluations"], 1)
+
+ completions = assignment["completions"]
+ self.assertEqual(
+ len(completions),
+ 3,
+ )
+
+ completion_1 = get_completion_for_user(completions, self.participant_1.user)
+ self.assertEqual(completion_1["status"], "EVALUATED")
+ self.assertEqual(completion_1["last_name"], "Participant_1")
+ self.assertEqual(completion_1["user_id"], str(self.participant_1.user.id))
+
+ completion_2 = get_completion_for_user(completions, self.participant_2.user)
+ self.assertEqual(completion_2["status"], "SUBMITTED")
+ self.assertEqual(completion_2["last_name"], "Participant_2")
+ self.assertEqual(completion_2["user_id"], str(self.participant_2.user.id))
+
+ completion_3 = get_completion_for_user(completions, self.participant_3.user)
+ self.assertEqual(completion_3["status"], "UNKNOWN")
+ self.assertEqual(completion_3["last_name"], "Participant_3")
+ self.assertEqual(completion_3["user_id"], str(self.participant_3.user.id))
+
+ def test_api_praxis_assignments(self) -> None:
+ # GIVEN
+ self.client.force_login(self.mentor)
+
+ assignment = create_assignment(
+ course=self.course, assignment_type=AssignmentType.PRAXIS_ASSIGNMENT
+ )
+
+ lca = create_assignment_learning_content(self.circle, assignment)
+ create_course_session_assignment(
+ course_session=self.course_session, learning_content_assignment=lca
+ )
+
+ mentor = LearningMentor.objects.create(
+ mentor=self.mentor,
+ course=self.course_session.course,
+ )
+
+ participants = [self.participant_1, self.participant_2, self.participant_3]
mentor.participants.set(participants)
AssignmentCompletion.objects.create(
assignment_user=self.participant_1.user,
course_session=self.course_session,
- assignment=self.assignment,
+ assignment=assignment,
completion_status=AssignmentCompletionStatus.EVALUATION_SUBMITTED.value,
evaluation_user=self.mentor,
)
@@ -131,7 +229,7 @@ class LearningMentorAPITest(APITestCase):
AssignmentCompletion.objects.create(
assignment_user=self.participant_3.user,
course_session=self.course_session,
- assignment=self.assignment,
+ assignment=assignment,
completion_status=AssignmentCompletionStatus.SUBMITTED.value,
evaluation_user=self.mentor,
)
diff --git a/server/vbv_lernwelt/learning_mentor/views.py b/server/vbv_lernwelt/learning_mentor/views.py
index 04abb592..e56c33fc 100644
--- a/server/vbv_lernwelt/learning_mentor/views.py
+++ b/server/vbv_lernwelt/learning_mentor/views.py
@@ -12,11 +12,14 @@ from vbv_lernwelt.iam.permissions import has_role_in_course, is_course_session_m
from vbv_lernwelt.learning_mentor.content.praxis_assignment import (
get_praxis_assignments,
)
+from vbv_lernwelt.learning_mentor.content.self_evaluation_feedback import (
+ get_self_feedback_evaluation,
+)
from vbv_lernwelt.learning_mentor.models import LearningMentor, MentorInvitation
from vbv_lernwelt.learning_mentor.serializers import (
InvitationSerializer,
+ MentorAssignmentStatusSerializer,
MentorSerializer,
- PraxisAssignmentStatusSerializer,
)
from vbv_lernwelt.learnpath.models import Circle
from vbv_lernwelt.notify.email.email_services import EmailTemplate, send_email
@@ -37,24 +40,40 @@ def mentor_summary(request, course_session_id: int):
assignments = []
circle_ids = set()
- praxis_assignments, _circle_ids = get_praxis_assignments(
- course_session=course_session, participants=users, evaluation_user=request.user
+ praxis_assignments, praxis_assignments_circle_ids = get_praxis_assignments(
+ course_session=course_session,
+ participants=users,
+ evaluation_user=request.user, # noqa
+ )
+
+ (
+ self_evaluation_feedbacks,
+ self_evaluation_feedback_circle_ids,
+ ) = get_self_feedback_evaluation(
+ participants=users,
+ evaluation_user=request.user, # noqa
+ course=course_session.course,
+ )
+
+ circle_ids.update(praxis_assignments_circle_ids)
+ circle_ids.update(self_evaluation_feedback_circle_ids)
+
+ assignments.extend(
+ MentorAssignmentStatusSerializer(praxis_assignments, many=True).data
)
assignments.extend(
- PraxisAssignmentStatusSerializer(praxis_assignments, many=True).data
+ MentorAssignmentStatusSerializer(self_evaluation_feedbacks, many=True).data
)
- circle_ids.update(_circle_ids)
-
- circles = Circle.objects.filter(id__in=circle_ids).values("id", "title")
assignments.sort(
key=lambda x: (-x.get("pending_evaluations", 0), x.get("title", "").lower())
)
-
return Response(
{
"participants": [UserSerializer(user).data for user in users],
- "circles": list(circles),
+ "circles": list(
+ Circle.objects.filter(id__in=circle_ids).values("id", "title")
+ ),
"assignments": assignments,
}
)
diff --git a/server/vbv_lernwelt/learnpath/create_vv_new_learning_path.py b/server/vbv_lernwelt/learnpath/create_vv_new_learning_path.py
index 29eee41e..a30d93be 100644
--- a/server/vbv_lernwelt/learnpath/create_vv_new_learning_path.py
+++ b/server/vbv_lernwelt/learnpath/create_vv_new_learning_path.py
@@ -5,11 +5,16 @@ from wagtail.rich_text import RichText
from wagtail_localize.models import LocaleSynchronization
from vbv_lernwelt.assignment.models import Assignment
-from vbv_lernwelt.competence.factories import PerformanceCriteriaFactory
+from vbv_lernwelt.competence.factories import (
+ ActionCompetenceFactory,
+ ActionCompetenceListPageFactory,
+ PerformanceCriteriaFactory,
+)
from vbv_lernwelt.competence.models import ActionCompetence
from vbv_lernwelt.core.admin import User
from vbv_lernwelt.course.consts import COURSE_VERSICHERUNGSVERMITTLERIN_ID
from vbv_lernwelt.course.models import CourseCategory, CoursePage
+from vbv_lernwelt.learnpath.models import LearningUnitPerformanceFeedbackType
from vbv_lernwelt.learnpath.tests.learning_path_factories import (
CircleFactory,
LearningContentAssignmentFactory,
@@ -43,7 +48,7 @@ def create_vv_new_learning_path(
)
TopicFactory(title="Basis", is_visible=False, parent=lp)
- create_circle_basis(lp)
+ create_circle_basis(lp, course_page=course_page)
TopicFactory(title="Gewinnen von Kunden", parent=lp)
create_circle_gewinnen(lp)
@@ -103,7 +108,7 @@ def create_vv_pruefung_learning_path(
Page.objects.update(owner=user)
-def create_circle_basis(lp, title="Basis"):
+def create_circle_basis(lp, title="Basis", course_page=None):
circle = CircleFactory(
title=title,
parent=lp,
@@ -145,10 +150,42 @@ def create_circle_basis(lp, title="Basis"):
)
LearningSequenceFactory(title="Arbeitsalltag", parent=circle)
- LearningUnitFactory(
+ lu = LearningUnitFactory(
title="Mein neuer Job, Arbeitstechnik, Soziale Medien, Datenschutz und Beratungspflichten",
+ feedback_user=LearningUnitPerformanceFeedbackType.MENTOR_FEEDBACK.name,
parent=circle,
)
+
+ competence_profile_page = ActionCompetenceListPageFactory(
+ title="KompetenzNavi",
+ parent=course_page,
+ )
+
+ ace = ActionCompetenceFactory(
+ parent=competence_profile_page,
+ )
+
+ PerformanceCriteriaFactory(
+ parent=ace,
+ competence_id="VV-Arbeitsalltag-A",
+ title="Ich kenne die wichtigsten Aspekte des Arbeitsalltags als Versicherungsvermittler/-in.",
+ learning_unit=lu,
+ )
+
+ PerformanceCriteriaFactory(
+ parent=ace,
+ competence_id="VV-Arbeitsalltag-B",
+ title="Ich identifiziere und analysiere neue Markttrends im Versicherungssektor.",
+ learning_unit=lu,
+ )
+
+ PerformanceCriteriaFactory(
+ parent=ace,
+ competence_id="VV-Arbeitsalltag-C",
+ title="Ich nutze digitale Tools zur Optimierung der Kundenbetreuung und -beratung im Versicherungswesen.",
+ learning_unit=lu,
+ )
+
LearningContentPlaceholderFactory(
title="Mediathek",
parent=circle,
diff --git a/server/vbv_lernwelt/learnpath/graphql/types.py b/server/vbv_lernwelt/learnpath/graphql/types.py
index 4e12cc4b..0cd23953 100644
--- a/server/vbv_lernwelt/learnpath/graphql/types.py
+++ b/server/vbv_lernwelt/learnpath/graphql/types.py
@@ -234,7 +234,7 @@ class LearningUnitObjectType(DjangoObjectType):
class Meta:
model = LearningUnit
interfaces = (CoursePageInterface,)
- fields = ["evaluate_url", "title_hidden"]
+ fields = ["evaluate_url", "title_hidden", "feedback_user"]
def resolve_evaluate_url(self: LearningUnit, info, **kwargs):
return self.get_evaluate_url()
diff --git a/server/vbv_lernwelt/learnpath/migrations/0013_auto_20240117_1400.py b/server/vbv_lernwelt/learnpath/migrations/0013_auto_20240117_1400.py
new file mode 100644
index 00000000..b8a11cb8
--- /dev/null
+++ b/server/vbv_lernwelt/learnpath/migrations/0013_auto_20240117_1400.py
@@ -0,0 +1,41 @@
+# Generated by Django 3.2.20 on 2024-01-17 13:00
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("learnpath", "0012_auto_20231129_0827"),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name="learningunit",
+ name="feedback_user",
+ field=models.CharField(
+ blank=True,
+ choices=[
+ ("NO_FEEDBACK", "NO_FEEDBACK"),
+ ("MENTOR_FEEDBACK", "MENTOR_FEEDBACK"),
+ ],
+ default="NO_FEEDBACK",
+ max_length=255,
+ ),
+ ),
+ migrations.AlterField(
+ model_name="learningcontentassignment",
+ name="assignment_type",
+ field=models.CharField(
+ choices=[
+ ("PRAXIS_ASSIGNMENT", "PRAXIS_ASSIGNMENT"),
+ ("CASEWORK", "CASEWORK"),
+ ("PREP_ASSIGNMENT", "PREP_ASSIGNMENT"),
+ ("REFLECTION", "REFLECTION"),
+ ("CONDITION_ACCEPTANCE", "CONDITION_ACCEPTANCE"),
+ ("EDONIQ_TEST", "EDONIQ_TEST"),
+ ],
+ default="CASEWORK",
+ max_length=50,
+ ),
+ ),
+ ]
diff --git a/server/vbv_lernwelt/learnpath/migrations/0014_alter_learningunit_feedback_user.py b/server/vbv_lernwelt/learnpath/migrations/0014_alter_learningunit_feedback_user.py
new file mode 100644
index 00000000..91dd1d39
--- /dev/null
+++ b/server/vbv_lernwelt/learnpath/migrations/0014_alter_learningunit_feedback_user.py
@@ -0,0 +1,24 @@
+# Generated by Django 3.2.20 on 2024-01-17 14:50
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("learnpath", "0013_auto_20240117_1400"),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name="learningunit",
+ name="feedback_user",
+ field=models.CharField(
+ choices=[
+ ("NO_FEEDBACK", "NO_FEEDBACK"),
+ ("MENTOR_FEEDBACK", "MENTOR_FEEDBACK"),
+ ],
+ default="NO_FEEDBACK",
+ max_length=255,
+ ),
+ ),
+ ]
diff --git a/server/vbv_lernwelt/learnpath/migrations/0015_set_feedback_user_mentor_for_vv.py b/server/vbv_lernwelt/learnpath/migrations/0015_set_feedback_user_mentor_for_vv.py
new file mode 100644
index 00000000..58bb55ec
--- /dev/null
+++ b/server/vbv_lernwelt/learnpath/migrations/0015_set_feedback_user_mentor_for_vv.py
@@ -0,0 +1,50 @@
+# Generated by Django 3.2.20 on 2024-02-12 15:19
+
+from django.db import migrations
+
+
+VV_COURSE_IDS_WITH_MENTOR_FEEDBACK = [
+ -4, # vv-de
+ -10, # vv-fr
+ -11, # vv-it
+]
+
+
+def is_learning_unit_in_vv_course(learning_unit):
+ return learning_unit.course_category.course_id in VV_COURSE_IDS_WITH_MENTOR_FEEDBACK
+
+
+def mutate_data(apps, schema_editor):
+ """
+ Enable feedback for learning units in VV courses, this means that on the self-assessment page
+ of the learning unit, the user can request feedback from the mentor.
+ """
+
+ LearningUnit = apps.get_model("learnpath", "LearningUnit") # noqa
+ for learning_unit in LearningUnit.objects.all():
+ if is_learning_unit_in_vv_course(learning_unit):
+ learning_unit.feedback_user = "MENTOR_FEEDBACK"
+ learning_unit.save()
+
+
+def rollback_data(apps, schema_editor):
+ """
+ Disable feedback for learning units in VV courses, this means that on the self-assessment page
+ of the learning unit, the user can not request feedback from the mentor. -> Default behaviour.
+ """
+
+ LearningUnit = apps.get_model("learnpath", "LearningUnit") # noqa
+ for learning_unit in LearningUnit.objects.all():
+ if is_learning_unit_in_vv_course(learning_unit):
+ learning_unit.feedback_user = "NO_FEEDBACK"
+ learning_unit.save()
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("learnpath", "0014_alter_learningunit_feedback_user"),
+ ]
+
+ operations = [
+ migrations.RunPython(mutate_data, rollback_data),
+ ]
diff --git a/server/vbv_lernwelt/learnpath/models.py b/server/vbv_lernwelt/learnpath/models.py
index 64d260cc..42b76a18 100644
--- a/server/vbv_lernwelt/learnpath/models.py
+++ b/server/vbv_lernwelt/learnpath/models.py
@@ -1,8 +1,10 @@
import re
+from enum import Enum
+from typing import Tuple
from django.db import models
from django.utils.text import slugify
-from wagtail.admin.panels import FieldPanel, PageChooserPanel
+from wagtail.admin.panels import FieldPanel, HelpPanel, PageChooserPanel
from wagtail.fields import RichTextField, StreamField
from wagtail.models import Page
@@ -117,6 +119,13 @@ class Circle(CourseBasePage):
return f"{self.title}"
+class LearningUnitPerformanceFeedbackType(Enum):
+ """Defines how feedback on the performance criteria (n) of a learning unit are given."""
+
+ NO_FEEDBACK = "NO_FEEDBACK"
+ MENTOR_FEEDBACK = "MENTOR_FEEDBACK"
+
+
class LearningSequence(CourseBasePage):
serialize_field_names = ["icon"]
@@ -169,10 +178,21 @@ class LearningUnit(CourseBasePage):
"course.CourseCategory", on_delete=models.SET_NULL, null=True, blank=True
)
title_hidden = models.BooleanField(default=False)
+ feedback_user = models.CharField(
+ max_length=255,
+ choices=[(tag.name, tag.name) for tag in LearningUnitPerformanceFeedbackType],
+ default=LearningUnitPerformanceFeedbackType.NO_FEEDBACK.name,
+ )
content_panels = Page.content_panels + [
FieldPanel("course_category"),
FieldPanel("title_hidden"),
+ FieldPanel("feedback_user"),
+ HelpPanel(
+ content="👆 Feedback zur Selbsteinschätzung: Normalerweise NO_FEEDBACK, "
+ "ausser bei den Lerninhalten Selbsteinschätzungen, die eine Bewertung haben von einer "
+ "Lernbegleitung haben sollen (z.B. VV)."
+ ),
]
class Meta:
@@ -200,21 +220,29 @@ class LearningUnit(CourseBasePage):
)
super(LearningUnit, self).save(clean, user, log_action, **kwargs)
- def get_frontend_url(self):
+ def get_frontend_url_parts(self) -> Tuple[str, str, str]:
+ """
+ Extracts the course, circle and learning unit part from the slug.
+ :return: Tuple of course, circle and learning unit part
+ """
+
r = re.compile(
r"^(?P.+?)-lp-circle-(?P.+?)-lu-(?P.+?)$"
)
m = r.match(self.slug)
+
if m is None:
- return "ERROR: could not parse slug"
- return f"/course/{m.group('coursePart')}/learn/{m.group('circlePart')}#lu-{m.group('luPart')}"
+ ValueError(f"Could not parse slug: {self.slug}")
+
+ return m.group("coursePart"), m.group("circlePart"), m.group("luPart")
+
+ def get_frontend_url(self):
+ course, circle, learning_unit = self.get_frontend_url_parts()
+ return f"/course/{course}/learn/{circle}#lu-{learning_unit}"
def get_evaluate_url(self):
- r = re.compile(
- r"^(?P.+?)-lp-circle-(?P.+?)-lu-(?P.+?)$"
- )
- m = r.match(self.slug)
- return f"/course/{m.group('coursePart')}/learn/{m.group('circlePart')}/evaluate/{m.group('luPart')}"
+ course, circle, learning_unit = self.get_frontend_url_parts()
+ return f"/course/{course}/learn/{circle}/evaluate/{learning_unit}"
def get_admin_display_title(self):
return f"LE: {self.draft_title}"
diff --git a/server/vbv_lernwelt/learnpath/serializers.py b/server/vbv_lernwelt/learnpath/serializers.py
index 3f6a3255..af616d38 100644
--- a/server/vbv_lernwelt/learnpath/serializers.py
+++ b/server/vbv_lernwelt/learnpath/serializers.py
@@ -20,6 +20,7 @@ class LearningUnitSerializer(
"course_category",
"children",
"title_hidden",
+ "feedback_user",
],
)
):
diff --git a/server/vbv_lernwelt/learnpath/tests/test_models.py b/server/vbv_lernwelt/learnpath/tests/test_models.py
index 416f5ce4..a5c198c4 100644
--- a/server/vbv_lernwelt/learnpath/tests/test_models.py
+++ b/server/vbv_lernwelt/learnpath/tests/test_models.py
@@ -2,6 +2,12 @@ from django.test import TestCase
from vbv_lernwelt.core.create_default_users import create_default_users
from vbv_lernwelt.course.creators.test_course import create_test_course
+from vbv_lernwelt.course.creators.test_utils import (
+ create_circle,
+ create_course,
+ create_course_session,
+ create_learning_unit,
+)
from vbv_lernwelt.learnpath.models import LearningContentPlaceholder
@@ -31,3 +37,22 @@ class SaveSlugTestCase(TestCase):
self.assertEqual(
lc_fachcheck.slug, "test-lehrgang-lp-circle-reisen-lc-fachcheck-foobar"
)
+
+ def test_learning_unit_frontend_url_parts(self):
+ # GIVEN
+ course, course_page = create_course(course_page_title="What Ever Course Page")
+ course_session = create_course_session(course=course, title=":)")
+ circle, _ = create_circle(title="A-nice Circle", course_page=course_page)
+
+ # WHEN
+ cut = create_learning_unit(
+ course_category_title="course category title",
+ circle=circle,
+ course=course,
+ )
+ course_part, circle_part, learning_unit_part = cut.get_frontend_url_parts()
+
+ # THEN
+ self.assertEqual(course_part, "what-ever-course-page")
+ self.assertEqual(circle_part, "a-nice-circle")
+ self.assertEqual(learning_unit_part, "course-category-title")
diff --git a/server/vbv_lernwelt/media_files/views.py b/server/vbv_lernwelt/media_files/views.py
index 96d624a4..55f37610 100644
--- a/server/vbv_lernwelt/media_files/views.py
+++ b/server/vbv_lernwelt/media_files/views.py
@@ -34,6 +34,8 @@ def user_image(request, image_id):
rendition.file.open("rb")
image_format = imghdr.what(rendition.file)
+
return StreamingHttpResponse(
- FileWrapper(rendition.file), content_type="image/" + image_format
+ FileWrapper(rendition.file),
+ content_type=f"image/{image_format}" if image_format else "binary/octet-stream",
)
diff --git a/server/vbv_lernwelt/notify/email/email_services.py b/server/vbv_lernwelt/notify/email/email_services.py
index 45144894..3dba1007 100644
--- a/server/vbv_lernwelt/notify/email/email_services.py
+++ b/server/vbv_lernwelt/notify/email/email_services.py
@@ -76,6 +76,20 @@ class EmailTemplate(Enum):
"it": "d-30c6aa9accda4973a940dd25703cb4a9",
}
+ # Fremdeinschätzung (Requester → Provider)
+ SELF_EVALUATION_FEEDBACK_REQUESTED = {
+ "de": "d-cf9c6681991d4293a7baccaa5b043c5c",
+ "fr": "d-6b103876807a4a0db6a0c31651c1e8ba",
+ "it": "d-403b2f9d09bb41dc9dc85eed6c35c942",
+ }
+
+ # Fremdeinschätzung (Provider → Requester)
+ SELF_EVALUATION_FEEDBACK_PROVIDED = {
+ "de": "d-e547bff40252458fa802759f2c502e3a",
+ "fr": "d-62aa7ce8639c49319f92edb858bbb1cd",
+ "it": "d-de2b5dfaf5d2470dbeea5d3ea2a6c442",
+ }
+
def send_email(
recipient_email: str,
diff --git a/server/vbv_lernwelt/notify/models.py b/server/vbv_lernwelt/notify/models.py
index 4e87ffea..70a846de 100644
--- a/server/vbv_lernwelt/notify/models.py
+++ b/server/vbv_lernwelt/notify/models.py
@@ -24,6 +24,13 @@ class NotificationTrigger(models.TextChoices):
CASEWORK_EVALUATED = "CASEWORK_EVALUATED", _("Casework Evaluated")
NEW_FEEDBACK = "NEW_FEEDBACK", _("New Feedback")
+ SELF_EVALUATION_FEEDBACK_REQUESTED = "SELF_EVALUATION_FEEDBACK_REQUESTED", _(
+ "Self Evaluation Feedback Requested"
+ )
+ SELF_EVALUATION_FEEDBACK_PROVIDED = "SELF_EVALUATION_FEEDBACK_PROVIDED", _(
+ "Self Evaluation Feedback Provided"
+ )
+
class Notification(AbstractNotification):
# UUIDs are not supported by the notifications app...
diff --git a/server/vbv_lernwelt/notify/services.py b/server/vbv_lernwelt/notify/services.py
index 8ce851d7..6f515b2b 100644
--- a/server/vbv_lernwelt/notify/services.py
+++ b/server/vbv_lernwelt/notify/services.py
@@ -23,6 +23,7 @@ from vbv_lernwelt.notify.models import (
NotificationCategory,
NotificationTrigger,
)
+from vbv_lernwelt.self_evaluation_feedback.models import SelfEvaluationFeedback
if TYPE_CHECKING:
from vbv_lernwelt.assignment.models import AssignmentCompletion
@@ -73,6 +74,74 @@ class NotificationService:
email_template=EmailTemplate.CASEWORK_SUBMITTED,
)
+ @classmethod
+ def send_self_evaluation_feedback_request_feedback_notification(
+ cls,
+ self_evaluation_feedback: SelfEvaluationFeedback,
+ ):
+ """Requester -> Provider"""
+ requester_user = self_evaluation_feedback.feedback_requester_user
+ provider_user = self_evaluation_feedback.feedback_provider_user
+
+ texts = {
+ "de": "%(requester)s hat eine Selbsteinschätzung mit dir geteilt",
+ "fr": "%(requester)s a partagé une auto-évaluation avec vous",
+ "it": "%(requester)s ha condiviso una valutazione personale con te",
+ }
+
+ verb = texts.get(provider_user.language, "de") % {
+ "requester": requester_user.get_full_name(),
+ }
+
+ return cls._send_notification(
+ recipient=provider_user,
+ verb=verb,
+ notification_category=NotificationCategory.USER_INTERACTION,
+ notification_trigger=NotificationTrigger.SELF_EVALUATION_FEEDBACK_REQUESTED,
+ sender=requester_user,
+ target_url=self_evaluation_feedback.feedback_provider_evaluation_url,
+ action_object=self_evaluation_feedback,
+ email_template=EmailTemplate.SELF_EVALUATION_FEEDBACK_REQUESTED,
+ template_data={
+ "mentee_name": requester_user.get_full_name(),
+ "mentee_email": requester_user.email,
+ },
+ )
+
+ @classmethod
+ def send_self_evaluation_feedback_received_notification(
+ cls,
+ self_evaluation_feedback: SelfEvaluationFeedback,
+ ):
+ """Provider -> Requester"""
+ requester_user = self_evaluation_feedback.feedback_requester_user
+ provider_user = self_evaluation_feedback.feedback_provider_user
+
+ texts = {
+ "de": "%(provider)s hat dir eine Fremdeinschätzung gegeben",
+ "fr": "%(provider)s vous a donné une évaluation externe",
+ "it": "%(provider)s ti ha dato una valutazione esterna",
+ }
+
+ verb = texts.get(requester_user.language, "de") % {
+ "provider": provider_user.get_full_name(),
+ }
+
+ return cls._send_notification(
+ recipient=requester_user,
+ verb=verb,
+ notification_category=NotificationCategory.USER_INTERACTION,
+ notification_trigger=NotificationTrigger.SELF_EVALUATION_FEEDBACK_PROVIDED,
+ sender=provider_user,
+ target_url=self_evaluation_feedback.feedback_requester_results_url,
+ action_object=self_evaluation_feedback,
+ email_template=EmailTemplate.SELF_EVALUATION_FEEDBACK_PROVIDED,
+ template_data={
+ "mentor_name": provider_user.get_full_name(),
+ "mentor_email": provider_user.email,
+ },
+ )
+
@classmethod
def send_assignment_evaluated_notification(
cls,
diff --git a/server/vbv_lernwelt/self_evaluation_feedback/__init__.py b/server/vbv_lernwelt/self_evaluation_feedback/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/server/vbv_lernwelt/self_evaluation_feedback/admin.py b/server/vbv_lernwelt/self_evaluation_feedback/admin.py
new file mode 100644
index 00000000..acabf04e
--- /dev/null
+++ b/server/vbv_lernwelt/self_evaluation_feedback/admin.py
@@ -0,0 +1,49 @@
+from django.contrib import admin
+
+from vbv_lernwelt.self_evaluation_feedback.models import (
+ CourseCompletionFeedback,
+ SelfEvaluationFeedback,
+)
+
+
+@admin.register(SelfEvaluationFeedback)
+class CourseSessionAdmin(admin.ModelAdmin):
+ list_display = (
+ "id",
+ "feedback_submitted",
+ "feedback_requester_user",
+ "feedback_provider_user",
+ "learning_unit",
+ )
+ list_filter = (
+ "feedback_submitted",
+ "feedback_requester_user",
+ "feedback_provider_user",
+ "learning_unit",
+ )
+ search_fields = (
+ "feedback_submitted",
+ "feedback_requester_user",
+ "feedback_provider_user",
+ "learning_unit",
+ )
+
+
+@admin.register(CourseCompletionFeedback)
+class CourseSessionAdmin(admin.ModelAdmin):
+ list_display = (
+ "id",
+ "feedback",
+ "course_completion",
+ "feedback_assessment",
+ )
+ list_filter = (
+ "feedback",
+ "course_completion",
+ "feedback_assessment",
+ )
+ search_fields = (
+ "feedback",
+ "course_completion",
+ "feedback_assessment",
+ )
diff --git a/server/vbv_lernwelt/self_evaluation_feedback/apps.py b/server/vbv_lernwelt/self_evaluation_feedback/apps.py
new file mode 100644
index 00000000..d0cec9eb
--- /dev/null
+++ b/server/vbv_lernwelt/self_evaluation_feedback/apps.py
@@ -0,0 +1,6 @@
+from django.apps import AppConfig
+
+
+class SelfEvaluationFeedbackConfig(AppConfig):
+ default_auto_field = "django.db.models.BigAutoField"
+ name = "vbv_lernwelt.self_evaluation_feedback"
diff --git a/server/vbv_lernwelt/self_evaluation_feedback/migrations/0001_initial.py b/server/vbv_lernwelt/self_evaluation_feedback/migrations/0001_initial.py
new file mode 100644
index 00000000..faa03afb
--- /dev/null
+++ b/server/vbv_lernwelt/self_evaluation_feedback/migrations/0001_initial.py
@@ -0,0 +1,113 @@
+# Generated by Django 3.2.20 on 2024-01-21 18:42
+
+import django.db.models.deletion
+from django.conf import settings
+from django.db import migrations, models
+
+import vbv_lernwelt.course.models
+
+
+class Migration(migrations.Migration):
+ initial = True
+
+ dependencies = [
+ ("course", "0006_auto_20231221_1411"),
+ ("learnpath", "0014_alter_learningunit_feedback_user"),
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name="SelfEvaluationFeedback",
+ fields=[
+ (
+ "id",
+ models.BigAutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ ("feedback_submitted", models.BooleanField(default=False)),
+ (
+ "feedback_provider_user",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="feedback_provider_user",
+ to=settings.AUTH_USER_MODEL,
+ ),
+ ),
+ (
+ "feedback_requester_user",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="feedback_requester_user",
+ to=settings.AUTH_USER_MODEL,
+ ),
+ ),
+ (
+ "learning_unit",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ to="learnpath.learningunit",
+ ),
+ ),
+ ],
+ ),
+ migrations.CreateModel(
+ name="CourseCompletionFeedback",
+ fields=[
+ (
+ "id",
+ models.BigAutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ (
+ "provider_evaluation_feedback",
+ 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,
+ ),
+ ),
+ (
+ "feedback",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ to="self_evaluation_feedback.selfevaluationfeedback",
+ ),
+ ),
+ (
+ "requester_evaluation",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ to="course.coursecompletion",
+ ),
+ ),
+ ],
+ ),
+ ]
diff --git a/server/vbv_lernwelt/self_evaluation_feedback/migrations/0002_rename_requester_evaluation_coursecompletionfeedback_course_completion.py b/server/vbv_lernwelt/self_evaluation_feedback/migrations/0002_rename_requester_evaluation_coursecompletionfeedback_course_completion.py
new file mode 100644
index 00000000..66eea762
--- /dev/null
+++ b/server/vbv_lernwelt/self_evaluation_feedback/migrations/0002_rename_requester_evaluation_coursecompletionfeedback_course_completion.py
@@ -0,0 +1,17 @@
+# Generated by Django 3.2.20 on 2024-01-22 13:20
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("self_evaluation_feedback", "0001_initial"),
+ ]
+
+ operations = [
+ migrations.RenameField(
+ model_name="coursecompletionfeedback",
+ old_name="requester_evaluation",
+ new_name="course_completion",
+ ),
+ ]
diff --git a/server/vbv_lernwelt/self_evaluation_feedback/migrations/0003_rename_provider_evaluation_feedback_coursecompletionfeedback_feedback_assessment.py b/server/vbv_lernwelt/self_evaluation_feedback/migrations/0003_rename_provider_evaluation_feedback_coursecompletionfeedback_feedback_assessment.py
new file mode 100644
index 00000000..1c803c26
--- /dev/null
+++ b/server/vbv_lernwelt/self_evaluation_feedback/migrations/0003_rename_provider_evaluation_feedback_coursecompletionfeedback_feedback_assessment.py
@@ -0,0 +1,20 @@
+# Generated by Django 3.2.20 on 2024-01-23 15:46
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ (
+ "self_evaluation_feedback",
+ "0002_rename_requester_evaluation_coursecompletionfeedback_course_completion",
+ ),
+ ]
+
+ operations = [
+ migrations.RenameField(
+ model_name="coursecompletionfeedback",
+ old_name="provider_evaluation_feedback",
+ new_name="feedback_assessment",
+ ),
+ ]
diff --git a/server/vbv_lernwelt/self_evaluation_feedback/migrations/__init__.py b/server/vbv_lernwelt/self_evaluation_feedback/migrations/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/server/vbv_lernwelt/self_evaluation_feedback/models.py b/server/vbv_lernwelt/self_evaluation_feedback/models.py
new file mode 100644
index 00000000..2bbee343
--- /dev/null
+++ b/server/vbv_lernwelt/self_evaluation_feedback/models.py
@@ -0,0 +1,44 @@
+from django.db import models
+
+from vbv_lernwelt.core.admin import User
+from vbv_lernwelt.course.models import CourseCompletion, CourseCompletionStatus
+
+
+class SelfEvaluationFeedback(models.Model):
+ feedback_submitted = models.BooleanField(default=False)
+
+ feedback_requester_user = models.ForeignKey(
+ User, on_delete=models.CASCADE, related_name="feedback_requester_user"
+ )
+
+ feedback_provider_user = models.ForeignKey(
+ User, on_delete=models.CASCADE, related_name="feedback_provider_user"
+ )
+
+ learning_unit = models.ForeignKey(
+ "learnpath.LearningUnit", on_delete=models.CASCADE
+ )
+
+ @property
+ def feedback_requester_results_url(self) -> str:
+ url = self.learning_unit.get_evaluate_url()
+ received_evaluation_step = len(self.learning_unit.performancecriteria_set.all())
+ return f"{url}?step={received_evaluation_step}"
+
+ @property
+ def feedback_provider_evaluation_url(self) -> str:
+ course, _, __ = self.learning_unit.get_frontend_url_parts()
+ return f"/course/{course}/cockpit/mentor/self-evaluation-feedback/{self.learning_unit.id}"
+
+
+class CourseCompletionFeedback(models.Model):
+ feedback = models.ForeignKey(SelfEvaluationFeedback, on_delete=models.CASCADE)
+
+ # the course completion has to be evaluated by the feedback provider
+ course_completion = models.ForeignKey(CourseCompletion, on_delete=models.CASCADE)
+
+ feedback_assessment = models.CharField(
+ max_length=255,
+ choices=[(status, status.value) for status in CourseCompletionStatus],
+ default=CourseCompletionStatus.UNKNOWN.value,
+ )
diff --git a/server/vbv_lernwelt/self_evaluation_feedback/serializers.py b/server/vbv_lernwelt/self_evaluation_feedback/serializers.py
new file mode 100644
index 00000000..fd24d363
--- /dev/null
+++ b/server/vbv_lernwelt/self_evaluation_feedback/serializers.py
@@ -0,0 +1,81 @@
+from typing import List
+
+from rest_framework import serializers
+
+from vbv_lernwelt.competence.models import PerformanceCriteria
+from vbv_lernwelt.core.serializers import UserSerializer
+from vbv_lernwelt.course.models import CourseCompletion, CourseCompletionStatus
+from vbv_lernwelt.self_evaluation_feedback.models import (
+ CourseCompletionFeedback,
+ SelfEvaluationFeedback,
+)
+
+
+class SelfEvaluationFeedbackSerializer(serializers.ModelSerializer):
+ criteria = serializers.SerializerMethodField()
+ feedback_requester_user = UserSerializer(read_only=True)
+ feedback_provider_user = UserSerializer(read_only=True)
+ learning_unit_id = serializers.PrimaryKeyRelatedField(
+ read_only=True, source="learning_unit"
+ )
+ feedback_id = serializers.PrimaryKeyRelatedField(read_only=True, source="id")
+ circle_name = serializers.SerializerMethodField()
+ title = serializers.CharField(source="learning_unit.title")
+
+ class Meta:
+ model = SelfEvaluationFeedback
+ fields = [
+ "feedback_id",
+ "title",
+ "circle_name",
+ "learning_unit_id",
+ "feedback_submitted",
+ "feedback_requester_user",
+ "feedback_provider_user",
+ "criteria",
+ ]
+
+ def get_circle_name(self, obj):
+ return obj.learning_unit.get_circle().title
+
+ def get_criteria(self, obj):
+ performance_criteria: List[
+ PerformanceCriteria
+ ] = obj.learning_unit.performancecriteria_set.all()
+
+ criteria = []
+
+ for pc in performance_criteria:
+ # requester self assessment
+ completion = CourseCompletion.objects.filter(
+ page_id=pc.id,
+ user=obj.feedback_requester_user,
+ ).first()
+
+ self_assessment = (
+ completion.completion_status
+ if completion
+ else CourseCompletionStatus.UNKNOWN.value
+ )
+
+ # provider feedback assessment
+ feedback = CourseCompletionFeedback.objects.filter(
+ course_completion=completion
+ ).first()
+
+ feedback_assessment = (
+ feedback.feedback_assessment
+ if feedback
+ else CourseCompletionStatus.UNKNOWN.value
+ )
+
+ criteria.append(
+ {
+ "course_completion_id": completion.id if completion else None,
+ "title": pc.title,
+ "self_assessment": self_assessment,
+ "feedback_assessment": feedback_assessment,
+ }
+ )
+
+ return criteria
diff --git a/server/vbv_lernwelt/self_evaluation_feedback/tests/__init__.py b/server/vbv_lernwelt/self_evaluation_feedback/tests/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/server/vbv_lernwelt/self_evaluation_feedback/tests/test_api.py b/server/vbv_lernwelt/self_evaluation_feedback/tests/test_api.py
new file mode 100644
index 00000000..98291bbc
--- /dev/null
+++ b/server/vbv_lernwelt/self_evaluation_feedback/tests/test_api.py
@@ -0,0 +1,463 @@
+from unittest.mock import patch
+
+from django.urls import reverse
+from rest_framework.test import APITestCase
+
+from vbv_lernwelt.course.creators.test_utils import (
+ add_course_session_user,
+ create_circle,
+ create_course,
+ create_course_session,
+ create_learning_unit,
+ create_performance_criteria_page,
+ create_user,
+)
+from vbv_lernwelt.course.models import CourseCompletionStatus, CourseSessionUser
+from vbv_lernwelt.course.services import mark_course_completion
+from vbv_lernwelt.learning_mentor.models import LearningMentor
+from vbv_lernwelt.self_evaluation_feedback.models import (
+ CourseCompletionFeedback,
+ SelfEvaluationFeedback,
+)
+
+
+def create_self_evaluation_feedback(
+ learning_unit, feedback_requester_user, feedback_provider_user
+):
+ return SelfEvaluationFeedback.objects.create(
+ learning_unit=learning_unit,
+ feedback_requester_user=feedback_requester_user,
+ feedback_provider_user=feedback_provider_user,
+ )
+
+
+class SelfEvaluationFeedbackAPI(APITestCase):
+ def setUp(self) -> None:
+ self.member = create_user("member")
+ self.mentor = create_user("mentor")
+
+ self.course, self.course_page = create_course("Test Course")
+ self.course_session = create_course_session(
+ course=self.course, title="Test Bern 2022 a"
+ )
+
+ member_csu = add_course_session_user(
+ course_session=self.course_session,
+ user=self.member,
+ role=CourseSessionUser.Role.MEMBER,
+ )
+
+ self.circle, _ = create_circle(
+ title="Test Circle", course_page=self.course_page
+ )
+
+ learning_mentor = LearningMentor.objects.create(
+ mentor=self.mentor,
+ course=self.course_session.course,
+ )
+
+ learning_mentor.participants.add(member_csu)
+
+ @patch(
+ "vbv_lernwelt.notify.services.NotificationService.send_self_evaluation_feedback_request_feedback_notification"
+ )
+ def test_start_self_evaluation_feedback(self, mock_notification_service_send):
+ # GIVEN
+ learning_unit = create_learning_unit(course=self.course, circle=self.circle)
+
+ pc = create_performance_criteria_page(
+ course=self.course,
+ course_page=self.course_page,
+ circle=self.circle,
+ learning_unit=learning_unit,
+ )
+
+ mark_course_completion(
+ page=pc,
+ user=self.member,
+ course_session=self.course_session,
+ completion_status="SUCCESS",
+ )
+
+ self.client.force_login(self.member)
+
+ # WHEN
+ response = self.client.post(
+ reverse(
+ "start_self_evaluation_feedback",
+ args=[learning_unit.id],
+ ),
+ {
+ "feedback_provider_user_id": self.mentor.id,
+ },
+ )
+
+ # make sure re-starting is a no-op
+ self.client.post(
+ reverse(
+ "start_self_evaluation_feedback",
+ args=[learning_unit.id],
+ ),
+ {
+ "feedback_provider_user_id": self.mentor.id,
+ },
+ )
+
+ # shall be idempotent
+ self.client.post(
+ reverse(
+ "start_self_evaluation_feedback",
+ args=[learning_unit.id],
+ ),
+ {
+ "feedback_provider_user_id": self.mentor.id,
+ },
+ )
+
+ # THEN
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response.data["success"], True)
+
+ self.assertEqual(
+ SelfEvaluationFeedback.objects.count(),
+ 1,
+ )
+
+ self_evaluation_feedback = SelfEvaluationFeedback.objects.first()
+ self.assertEqual(self_evaluation_feedback.feedback_requester_user, self.member)
+ self.assertEqual(self_evaluation_feedback.feedback_provider_user, self.mentor)
+ self.assertEqual(self_evaluation_feedback.learning_unit, learning_unit)
+
+ mock_notification_service_send.assert_called_once_with(
+ self_evaluation_feedback=self_evaluation_feedback
+ )
+
+ def test_start_self_evaluation_feedback_not_allowed_user(self):
+ # GIVEN
+ learning_unit = create_learning_unit(course=self.course, circle=self.circle)
+ not_a_mentor = create_user("not_a_mentor")
+
+ self.client.force_login(self.member)
+
+ # WHEN
+ response = self.client.post(
+ reverse("start_self_evaluation_feedback", args=[learning_unit.id]),
+ {
+ "feedback_provider_user_id": not_a_mentor.id,
+ },
+ )
+
+ # THEN
+ self.assertEqual(response.status_code, 403)
+
+ def test_get_self_evaluation_feedback_as_requester(self):
+ """Tests endpoint of feedback REQUESTER"""
+
+ # GIVEN
+ learning_unit = create_learning_unit(course=self.course, circle=self.circle)
+
+ performance_criteria_1 = create_performance_criteria_page(
+ course=self.course,
+ course_page=self.course_page,
+ circle=self.circle,
+ learning_unit=learning_unit,
+ )
+
+ create_performance_criteria_page(
+ course=self.course,
+ course_page=self.course_page,
+ circle=self.circle,
+ learning_unit=learning_unit,
+ )
+
+ completion = mark_course_completion(
+ page=performance_criteria_1,
+ user=self.member,
+ course_session=self.course_session,
+ completion_status=CourseCompletionStatus.SUCCESS.value,
+ )
+
+ self_evaluation_feedback = create_self_evaluation_feedback(
+ learning_unit=learning_unit,
+ feedback_requester_user=self.member,
+ feedback_provider_user=self.mentor,
+ )
+
+ CourseCompletionFeedback.objects.create(
+ feedback=self_evaluation_feedback,
+ course_completion=completion,
+ feedback_assessment=CourseCompletionStatus.FAIL.value,
+ )
+
+ self.client.force_login(self.member)
+
+ # WHEN
+ response = self.client.get(
+ reverse(
+ "get_self_evaluation_feedback_as_requester",
+ args=[learning_unit.id],
+ )
+ )
+
+ # THEN
+ self.assertEqual(response.status_code, 200)
+
+ feedback = response.data
+ self.assertEqual(feedback["learning_unit_id"], learning_unit.id)
+ self.assertEqual(feedback["feedback_submitted"], False)
+ self.assertEqual(feedback["circle_name"], self.circle.title)
+
+ provider_user = feedback["feedback_provider_user"]
+ self.assertEqual(provider_user["id"], str(self.mentor.id)) # noqa
+ self.assertEqual(provider_user["first_name"], self.mentor.first_name)
+ self.assertEqual(provider_user["last_name"], self.mentor.last_name)
+ self.assertEqual(provider_user["avatar_url"], self.mentor.avatar_url)
+
+ requester_user = feedback["feedback_requester_user"]
+ self.assertEqual(requester_user["id"], str(self.member.id)) # noqa
+ self.assertEqual(requester_user["first_name"], self.member.first_name)
+ self.assertEqual(requester_user["last_name"], self.member.last_name)
+ self.assertEqual(requester_user["avatar_url"], self.member.avatar_url)
+
+ self.assertEqual(len(feedback["criteria"]), 2)
+
+ first_criteria = feedback["criteria"][0]
+ self.assertEqual(first_criteria["course_completion_id"], completion.id)
+ self.assertEqual(first_criteria["title"], performance_criteria_1.title)
+ self.assertEqual(
+ first_criteria["self_assessment"],
+ CourseCompletionStatus.SUCCESS.value,
+ )
+ self.assertEqual(
+ first_criteria["feedback_assessment"], CourseCompletionStatus.FAIL.value
+ )
+
+ second_criteria = feedback["criteria"][1]
+ self.assertEqual(second_criteria["course_completion_id"], None)
+ self.assertEqual(second_criteria["title"], performance_criteria_1.title)
+ self.assertEqual(
+ second_criteria["self_assessment"], CourseCompletionStatus.UNKNOWN.value
+ )
+ self.assertEqual(
+ second_criteria["feedback_assessment"],
+ CourseCompletionStatus.UNKNOWN.value,
+ )
+
+ def test_get_self_evaluation_feedback_as_provider(self):
+ """Tests endpoint of feedback PROVIDER"""
+
+ # GIVEN
+ learning_unit = create_learning_unit(course=self.course, circle=self.circle)
+
+ performance_criteria_1 = create_performance_criteria_page(
+ course=self.course,
+ course_page=self.course_page,
+ circle=self.circle,
+ learning_unit=learning_unit,
+ )
+
+ create_performance_criteria_page(
+ course=self.course,
+ course_page=self.course_page,
+ circle=self.circle,
+ learning_unit=learning_unit,
+ )
+
+ completion = mark_course_completion(
+ page=performance_criteria_1,
+ user=self.member,
+ course_session=self.course_session,
+ completion_status=CourseCompletionStatus.SUCCESS.value,
+ )
+
+ self_evaluation_feedback = create_self_evaluation_feedback(
+ learning_unit=learning_unit,
+ feedback_requester_user=self.member,
+ feedback_provider_user=self.mentor,
+ )
+
+ CourseCompletionFeedback.objects.create(
+ feedback=self_evaluation_feedback,
+ course_completion=completion,
+ feedback_assessment=CourseCompletionStatus.FAIL.value,
+ )
+
+ self.client.force_login(self.mentor)
+
+ # WHEN
+ response = self.client.get(
+ reverse(
+ "get_self_evaluation_feedback_as_provider",
+ args=[self_evaluation_feedback.learning_unit.id],
+ )
+ )
+
+ # THEN
+ self.assertEqual(response.status_code, 200)
+
+ feedback = response.data
+ self.assertEqual(feedback["learning_unit_id"], learning_unit.id)
+ self.assertEqual(feedback["title"], learning_unit.title)
+ self.assertEqual(feedback["feedback_submitted"], False)
+ self.assertEqual(feedback["circle_name"], self.circle.title)
+
+ provider_user = feedback["feedback_provider_user"]
+ self.assertEqual(provider_user["id"], str(self.mentor.id)) # noqa
+ self.assertEqual(provider_user["first_name"], self.mentor.first_name)
+ self.assertEqual(provider_user["last_name"], self.mentor.last_name)
+ self.assertEqual(provider_user["avatar_url"], self.mentor.avatar_url)
+
+ requester_user = feedback["feedback_requester_user"]
+ self.assertEqual(requester_user["id"], str(self.member.id)) # noqa
+ self.assertEqual(requester_user["first_name"], self.member.first_name)
+ self.assertEqual(requester_user["last_name"], self.member.last_name)
+ self.assertEqual(requester_user["avatar_url"], self.member.avatar_url)
+
+ self.assertEqual(len(feedback["criteria"]), 2)
+
+ first_criteria = feedback["criteria"][0]
+ self.assertEqual(first_criteria["course_completion_id"], completion.id)
+ self.assertEqual(first_criteria["title"], performance_criteria_1.title)
+ self.assertEqual(
+ first_criteria["self_assessment"],
+ CourseCompletionStatus.SUCCESS.value,
+ )
+ self.assertEqual(
+ first_criteria["feedback_assessment"], CourseCompletionStatus.FAIL.value
+ )
+
+ second_criteria = feedback["criteria"][1]
+ self.assertEqual(second_criteria["course_completion_id"], None)
+ self.assertEqual(second_criteria["title"], performance_criteria_1.title)
+ self.assertEqual(
+ second_criteria["self_assessment"], CourseCompletionStatus.UNKNOWN.value
+ )
+ self.assertEqual(
+ second_criteria["feedback_assessment"],
+ CourseCompletionStatus.UNKNOWN.value,
+ )
+
+ def test_self_evaluation_feedback_assessment(self):
+ # GIVEN
+ learning_unit = create_learning_unit(course=self.course, circle=self.circle)
+
+ performance_criteria_1 = create_performance_criteria_page(
+ course=self.course,
+ course_page=self.course_page,
+ circle=self.circle,
+ learning_unit=learning_unit,
+ )
+
+ completion = mark_course_completion(
+ page=performance_criteria_1,
+ user=self.member,
+ course_session=self.course_session,
+ completion_status=CourseCompletionStatus.SUCCESS.value,
+ )
+
+ self_evaluation_feedback = create_self_evaluation_feedback(
+ learning_unit=learning_unit,
+ feedback_requester_user=self.member,
+ feedback_provider_user=self.mentor,
+ )
+
+ self.client.force_login(self.mentor)
+
+ # WHEN
+ response = self.client.put(
+ reverse(
+ "add_self_evaluation_feedback_assessment",
+ args=[self_evaluation_feedback.id],
+ ),
+ {
+ "course_completion_id": completion.id,
+ "feedback_assessment": CourseCompletionStatus.FAIL.value,
+ },
+ )
+
+ # THEN
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response.data["success"], True)
+
+ feedback = CourseCompletionFeedback.objects.get(
+ feedback=self_evaluation_feedback,
+ course_completion=completion,
+ )
+
+ self.assertEqual(
+ feedback.feedback_assessment, CourseCompletionStatus.FAIL.value
+ )
+
+ @patch(
+ "vbv_lernwelt.notify.services.NotificationService.send_self_evaluation_feedback_received_notification"
+ )
+ def test_release_self_evaluation_feedback(self, mock_notification_service_send):
+ # GIVEN
+ learning_unit = create_learning_unit(course=self.course, circle=self.circle)
+ self_evaluation_feedback = create_self_evaluation_feedback(
+ learning_unit=learning_unit,
+ feedback_requester_user=self.member,
+ feedback_provider_user=self.mentor,
+ )
+
+ self.assertEqual(self_evaluation_feedback.feedback_submitted, False)
+ self.client.force_login(self.mentor)
+
+ # WHEN
+ response = self.client.put(
+ reverse(
+ "release_self_evaluation_feedback", args=[self_evaluation_feedback.id]
+ ),
+ )
+
+ # shall be idempotent
+ response = self.client.put(
+ reverse(
+ "release_self_evaluation_feedback", args=[self_evaluation_feedback.id]
+ ),
+ )
+
+ # THEN
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response.data["success"], True)
+ self.assertEqual(
+ SelfEvaluationFeedback.objects.get(
+ id=self_evaluation_feedback.id
+ ).feedback_submitted,
+ True,
+ )
+
+ mock_notification_service_send.assert_called_once_with(
+ self_evaluation_feedback=self_evaluation_feedback
+ )
+
+ def test_get_self_evaluation_feedback_frontend_urls(self):
+ """Makes sure that the frontend urls are correct (used in notifications)"""
+ # GIVEN
+ learning_unit = create_learning_unit(course=self.course, circle=self.circle)
+
+ cut = create_self_evaluation_feedback(
+ learning_unit=learning_unit,
+ feedback_requester_user=self.member,
+ feedback_provider_user=self.mentor,
+ )
+
+ # WHEN
+ requester_url = cut.feedback_requester_results_url
+ provider_url = cut.feedback_provider_evaluation_url
+
+ # THEN
+ _course, _circle, _learning_unit = learning_unit.get_frontend_url_parts()
+
+ # 0 -> no completions so step=0 is correct
+ _step = len(learning_unit.performancecriteria_set.all())
+
+ self.assertEqual(
+ requester_url,
+ f"/course/{_course}/learn/{_circle}/evaluate/{_learning_unit}?step={_step}",
+ )
+
+ self.assertEqual(
+ provider_url,
+ f"/course/{_course}/cockpit/mentor/self-evaluation-feedback/{learning_unit.id}",
+ )
diff --git a/server/vbv_lernwelt/self_evaluation_feedback/urls.py b/server/vbv_lernwelt/self_evaluation_feedback/urls.py
new file mode 100644
index 00000000..0b0514de
--- /dev/null
+++ b/server/vbv_lernwelt/self_evaluation_feedback/urls.py
@@ -0,0 +1,37 @@
+from django.urls import path
+
+from vbv_lernwelt.self_evaluation_feedback.views import (
+ add_provider_self_evaluation_feedback,
+ get_self_evaluation_feedback_as_provider,
+ get_self_evaluation_feedback_as_requester,
+ release_provider_self_evaluation_feedback,
+ start_self_evaluation_feedback,
+)
+
+urlpatterns = [
+ path(
+ "requester//feedback/start",
+ start_self_evaluation_feedback,
+ name="start_self_evaluation_feedback",
+ ),
+ path(
+ "requester//feedback",
+ get_self_evaluation_feedback_as_requester,
+ name="get_self_evaluation_feedback_as_requester",
+ ),
+ path(
+ "provider//feedback",
+ get_self_evaluation_feedback_as_provider,
+ name="get_self_evaluation_feedback_as_provider",
+ ),
+ path(
+ "provider/feedback//release",
+ release_provider_self_evaluation_feedback,
+ name="release_self_evaluation_feedback",
+ ),
+ path(
+ "provider/feedback//add-assessment",
+ add_provider_self_evaluation_feedback,
+ name="add_self_evaluation_feedback_assessment",
+ ),
+]
diff --git a/server/vbv_lernwelt/self_evaluation_feedback/views.py b/server/vbv_lernwelt/self_evaluation_feedback/views.py
new file mode 100644
index 00000000..2bb02376
--- /dev/null
+++ b/server/vbv_lernwelt/self_evaluation_feedback/views.py
@@ -0,0 +1,125 @@
+from django.shortcuts import get_object_or_404
+from rest_framework.decorators import api_view, permission_classes
+from rest_framework.exceptions import PermissionDenied
+from rest_framework.permissions import IsAuthenticated
+from rest_framework.response import Response
+
+from vbv_lernwelt.core.models import User
+from vbv_lernwelt.course.models import CourseCompletion
+from vbv_lernwelt.learning_mentor.models import LearningMentor
+from vbv_lernwelt.learnpath.models import LearningUnit
+from vbv_lernwelt.notify.services import NotificationService
+from vbv_lernwelt.self_evaluation_feedback.models import (
+ CourseCompletionFeedback,
+ SelfEvaluationFeedback,
+)
+from vbv_lernwelt.self_evaluation_feedback.serializers import (
+ SelfEvaluationFeedbackSerializer,
+)
+
+
+@api_view(["POST"])
+@permission_classes([IsAuthenticated])
+def start_self_evaluation_feedback(request, learning_unit_id):
+ feedback_provider_user_id = request.data.get("feedback_provider_user_id")
+
+ learning_unit = get_object_or_404(LearningUnit, id=learning_unit_id)
+ feedback_provider_user = get_object_or_404(User, id=feedback_provider_user_id)
+
+ if not LearningMentor.objects.filter(
+ course=learning_unit.get_course(),
+ mentor=feedback_provider_user,
+ participants__user=request.user,
+ ).exists():
+ raise PermissionDenied()
+
+ # calling start multiple times shall be a no-op
+ feedback, created = SelfEvaluationFeedback.objects.get_or_create(
+ feedback_requester_user=request.user,
+ feedback_provider_user=feedback_provider_user,
+ learning_unit=learning_unit,
+ )
+
+ if created:
+ NotificationService.send_self_evaluation_feedback_request_feedback_notification(
+ self_evaluation_feedback=feedback
+ )
+
+ return Response({"success": True})
+
+
+@api_view(["PUT"])
+@permission_classes([IsAuthenticated])
+def release_provider_self_evaluation_feedback(request, feedback_id):
+ feedback = get_object_or_404(
+ SelfEvaluationFeedback, id=feedback_id, feedback_provider_user=request.user
+ )
+
+ if feedback.feedback_submitted:
+ return Response({"success": True})
+
+ feedback.feedback_submitted = True
+ feedback.save()
+
+ NotificationService.send_self_evaluation_feedback_received_notification(
+ self_evaluation_feedback=feedback
+ )
+
+ return Response({"success": True})
+
+
+@api_view(["GET"])
+@permission_classes([IsAuthenticated])
+def get_self_evaluation_feedback_as_provider(request, learning_unit_id):
+ feedback = get_object_or_404(
+ SelfEvaluationFeedback,
+ learning_unit_id=learning_unit_id,
+ feedback_provider_user=request.user,
+ )
+
+ return Response(SelfEvaluationFeedbackSerializer(feedback).data)
+
+
+@api_view(["GET"])
+@permission_classes([IsAuthenticated])
+def get_self_evaluation_feedback_as_requester(request, learning_unit_id):
+ learning_unit = get_object_or_404(LearningUnit, id=learning_unit_id)
+
+ feedback = get_object_or_404(
+ SelfEvaluationFeedback,
+ learning_unit=learning_unit,
+ feedback_requester_user=request.user,
+ )
+
+ return Response(SelfEvaluationFeedbackSerializer(feedback).data)
+
+
+@api_view(["PUT"])
+@permission_classes([IsAuthenticated])
+def add_provider_self_evaluation_feedback(request, feedback_id):
+ feedback_assessment = request.data.get("feedback_assessment")
+
+ feedback = get_object_or_404(
+ SelfEvaluationFeedback, id=feedback_id, feedback_provider_user=request.user
+ )
+
+ course_completion = get_object_or_404(
+ CourseCompletion,
+ id=request.data.get("course_completion_id"),
+ user=feedback.feedback_requester_user,
+ )
+
+ (
+ course_completion_feedback,
+ created,
+ ) = CourseCompletionFeedback.objects.get_or_create(
+ feedback=feedback,
+ course_completion=course_completion,
+ defaults={"feedback_assessment": feedback_assessment},
+ )
+
+ if not created:
+ course_completion_feedback.feedback_assessment = feedback_assessment
+ course_completion_feedback.save()
+
+ return Response({"success": True})
diff --git a/server/vbv_lernwelt/templates/admin/index.html b/server/vbv_lernwelt/templates/admin/index.html
index 32d3e5f2..d6cabe3f 100644
--- a/server/vbv_lernwelt/templates/admin/index.html
+++ b/server/vbv_lernwelt/templates/admin/index.html
@@ -47,6 +47,12 @@