feat: mentor feedback

This commit is contained in:
Livio Bieri 2024-01-30 22:58:53 +01:00
parent abb371d4a0
commit f228f9ee47
12 changed files with 271 additions and 67 deletions

View File

@ -0,0 +1,72 @@
<script setup lang="ts">
import type { Criterion } from "@/services/selfEvaluationFeedback";
import type { User } from "@/types";
import { computed } from "vue";
const props = defineProps<{
criteria: Criterion;
requester: User;
}>();
const emit = defineEmits(["evaluation"]);
const fullname = computed(
() => `${props.requester.first_name} ${props.requester.last_name}`
);
const description = computed(() => `«${props.criteria.title}»`);
const currentEvaluation = computed(() => props.criteria.feedback_assessment);
const onPassed = () => {
emit("evaluation", props.criteria, "SUCCESS");
};
const onFailed = () => {
emit("evaluation", props.criteria, "FAIL");
};
</script>
<template>
<div class="mt-16 space-y-4 bg-gray-200 p-7">
<span>{{ $t("a.Beurteilungskriterium") }}:</span>
<div class="text-bold text-xl">{{ description }}</div>
</div>
<div class="mt-16 flex flex-row items-center pb-4">
<span class="text-2xl font-bold">
{{ $t("a.Kann FULLNAME das?", { FULLNAME: fullname }) }}
</span>
<img class="ml-4 h-12 w-12 rounded" :src="requester.avatar_url" :alt="fullname" />
</div>
<div class="flex space-x-10">
<button
class="inline-flex flex-1 items-center border-2 p-4 text-left"
:class="currentEvaluation === 'SUCCESS' ? 'border-green-500' : 'border-gray-300'"
@click="onPassed"
>
<it-icon-smiley-happy class="mr-4 h-16 w-16"></it-icon-smiley-happy>
<span class="text-lg font-bold">
{{
$t("a.Ja, NAME kann das.", {
NAME: requester.first_name,
})
}}
</span>
</button>
<button
class="inline-flex flex-1 items-center border-2 p-4 text-left"
:class="currentEvaluation === 'FAIL' ? 'border-orange-500' : 'border-gray-300'"
@click="onFailed"
>
<it-icon-smiley-thinking class="mr-4 h-16 w-16"></it-icon-smiley-thinking>
<span class="text-lg font-bold">
{{
$t("a.Nein, NAME muss das nochmals anschauen.", {
NAME: requester.first_name,
})
}}
</span>
</button>
</div>
</template>
<style scoped></style>

View File

@ -0,0 +1,20 @@
<script setup lang="ts">
import type { FeedbackRequest } from "@/services/selfEvaluationFeedback";
defineProps<{
feedback: FeedbackRequest;
}>();
const emit = defineEmits(["release"]);
</script>
<template>
<button
v-if="!feedback.feedback_submitted"
class="inline-flex flex-1 items-center border-2 p-4 text-left"
@click="emit('release')"
/>
<span v-else>Freigeben</span>
</template>
<style scoped></style>

View File

@ -31,10 +31,20 @@ defineProps<{
<it-icon-smiley-happy class="h-6 w-6" />
<span>{{ $t("selfEvaluation.yes") }}</span>
</div>
<div v-else class="flex flex-row items-center space-x-2">
<div
v-else-if="completion.completion_status == 'FAIL'"
class="flex flex-row items-center space-x-2"
>
<it-icon-smiley-thinking class="h-6 w-6" />
<span>{{ $t("selfEvaluation.no") }}</span>
</div>
<div
v-else-if="completion.completion_status == 'UNKNOWN'"
class="flex flex-row items-center space-x-2"
>
<it-icon-smiley-neutral class="h-6 w-6" />
<span>{{ $t("a.Nicht bewertet") }}</span>
</div>
</div>
</template>

View File

@ -2,9 +2,14 @@
import LearningContentMultiLayout from "@/pages/learningPath/learningContentPage/layouts/LearningContentMultiLayout.vue";
import LearningContentContainer from "@/pages/learningPath/learningContentPage/LearningContentContainer.vue";
import { useRouter } from "vue-router";
import { computed, onMounted, ref, toValue } from "vue";
import { useCSRFFetch } from "@/fetchHelpers";
import type { FeedbackRequest } from "@/services/selfEvaluationFeedback";
import { computed, ref } from "vue";
import {
type Criterion,
useSelfEvaluationFeedback,
} from "@/services/selfEvaluationFeedback";
import { useRouteQuery } from "@vueuse/router";
import FeedbackProviderRankCriteria from "@/components/selfEvaluationFeedback/FeedbackProviderRankCriteria.vue";
import FeedbackProviderReleaseOverview from "@/components/selfEvaluationFeedback/FeedbackProviderReleaseOverview.vue";
const router = useRouter();
const props = defineProps<{
@ -12,47 +17,62 @@ const props = defineProps<{
courseSlug: string;
}>();
const url = computed(
() =>
`/api/self-evaluation-feedback/provider/${toValue(props.learningUnitId)}/feedback`
);
const feedback = ref<FeedbackRequest | null>();
const isFeedbackLoading = ref(false);
onMounted(async () => {
feedback.value = null;
isFeedbackLoading.value = true;
const { data, error } = await useCSRFFetch(url.value).json();
isFeedbackLoading.value = false;
if (error.value) {
console.error(error.value);
return;
} else {
feedback.value = data.value;
}
const currentStepRouteParam = useRouteQuery("step", "0", {
transform: Number,
mode: "push",
});
const stepsCount = computed(() => 10);
const currentStep = 1;
const selfEvaluationFeedback = useSelfEvaluationFeedback(
props.learningUnitId,
"provider"
);
const title = "TITLE " + props.learningUnitId;
const feedback = computed(() => selfEvaluationFeedback?.feedback.value);
const showNextButton = true;
const showExitButton = true;
const showPreviousButton = true;
const base_url = "BASE_URL";
const title = computed(() => {
if (feedback.value) {
return feedback.value.title;
}
return "";
});
const endBadgeText = "END_BADGE_TEXT";
const currentStep = ref(currentStepRouteParam);
const stepsCount = computed(() => {
if (feedback.value) {
return feedback.value.criteria.length + 1;
}
return 0;
});
const currentCriteria = computed(() => {
if (feedback.value && currentStep.value < stepsCount.value - 1) {
return feedback.value.criteria[currentStep.value];
}
return null;
});
const showNextButton = computed(() => {
if (feedback.value) {
return currentStep.value < stepsCount.value - 1;
}
return false;
});
const handleBack = () => {
console.log("handleBack");
if (currentStep.value > 0) {
currentStep.value--;
}
};
const handleContinue = () => {
console.log("handleContinue");
if (currentStep.value < stepsCount.value) {
currentStep.value++;
}
};
const clickExit = () => {
console.log("clickExit");
router.push({
name: "mentorCockpitSelfEvaluationFeedbackAssignments",
params: {
@ -60,35 +80,60 @@ const clickExit = () => {
},
});
};
const handleFeedbackEvaluation = async (
criteria: Criterion,
evaluation: "SUCCESS" | "FAIL"
) => {
if (!feedback.value) {
return;
}
await selfEvaluationFeedback.addFeedbackAssessment(
criteria.course_completion_id,
evaluation
);
};
const handleFeedbackRelease = async () => {
if (!feedback.value) {
return;
}
await selfEvaluationFeedback.releaseFeedback();
};
</script>
<template>
<LearningContentContainer v-if="feedback" @exit="clickExit">
<LearningContentMultiLayout
icon="it-icon-lc-learning-module"
close-button-variant="close"
:current-step="currentStep"
:sub-title="$t('a.Selbsteinschätzung')"
:title="title"
icon="it-icon-lc-learning-module"
:steps-count="stepsCount"
:show-next-button="showNextButton"
:show-exit-button="showExitButton"
:show-exit-button="!showNextButton"
:show-start-button="false"
:show-previous-button="showPreviousButton"
:base-url="base_url"
:end-badge-text="endBadgeText"
@exit="clickExit"
:show-previous-button="currentStep > 0"
:end-badge-text="$t('general.submission')"
@exit="clickExit()"
@previous="handleBack()"
@next="handleContinue()"
>
<div v-if="currentStep" class="h-full">
<div class="mt-8">
<h3 class="heading-3">{{ "Some title " }}}</h3>
<div
class="mt-4 flex flex-col justify-between gap-8 lg:mt-8 lg:flex-row lg:gap-12"
>
<div>TEST {{ feedback }}</div>
</div>
</div>
<div v-if="feedback" class="h-full">
<!-- Performance Criteria Evaluation -->
<FeedbackProviderRankCriteria
v-if="currentCriteria"
:requester="feedback.feedback_requester_user"
:criteria="currentCriteria"
@evaluation="handleFeedbackEvaluation"
/>
<!-- Submission -->
<FeedbackProviderReleaseOverview
v-else
:feedback="feedback"
@release="handleFeedbackRelease"
/>
</div>
</LearningContentMultiLayout>
</LearningContentContainer>

View File

@ -51,6 +51,13 @@ const showExitButton = computed(
function handleContinue() {
log.debug("handleContinue");
// 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;

View File

@ -15,7 +15,10 @@ const props = defineProps<{
criteria: LearningUnitPerformanceCriteria[];
}>();
const selfEvaluationFeedback = useSelfEvaluationFeedback(props.learningUnit.id);
const selfEvaluationFeedback = useSelfEvaluationFeedback(
props.learningUnit.id,
"requester"
);
const storedFeedback = computed(() => selfEvaluationFeedback.feedback.value);
const isStoredFeedbackLoading = computed(() => selfEvaluationFeedback.loading.value);

View File

@ -1,11 +1,13 @@
import { useCSRFFetch } from "@/fetchHelpers";
import type { User } from "@/types";
import type { Ref } from "vue";
import { computed, onMounted, ref, toValue } 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;
@ -13,26 +15,31 @@ export interface FeedbackRequest {
criteria: Criterion[];
}
interface Criterion {
export interface Criterion {
course_completion_id: string;
title: string;
self_assessment: "FAIL" | "SUCCESS" | "UNKNOWN";
feedback_assessment: "FAIL" | "SUCCESS" | "UNKNOWN";
}
export function useSelfEvaluationFeedback(learningUnitId: Ref<string> | string) {
/** 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
*/
export function useSelfEvaluationFeedback(
learningUnitId: Ref<string> | string,
feedbackRole: "requester" | "provider"
) {
const feedback = ref<FeedbackRequest>();
const loading = ref(false);
const error = ref();
const url = computed(
() => `/api/self-evaluation-feedback/requester/${toValue(learningUnitId)}/feedback`
() => `/api/self-evaluation-feedback/${feedbackRole}/${learningUnitId}/feedback`
);
const fetchSelfEvaluationFeedback = async () => {
feedback.value = undefined;
const fetch = async () => {
error.value = undefined;
loading.value = true;
const { data, statusCode, error: _error } = await useCSRFFetch(url.value).json();
loading.value = false;
@ -50,11 +57,50 @@ export function useSelfEvaluationFeedback(learningUnitId: Ref<string> | string)
}
};
onMounted(fetchSelfEvaluationFeedback);
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,
});
// KISS: the backend is our "store" so just
// re-fetch e whole feedback from the backend
await fetch();
};
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({});
// KISS: the backend is our "store" so just
// re-fetch e whole feedback from the backend
await fetch();
};
onMounted(fetch);
return {
feedback,
error,
loading,
// feedback provider actions
addFeedbackAssessment,
releaseFeedback,
};
}

View File

@ -37,6 +37,7 @@ def create_blank_completions_non_requesters(
status=MentorCompletionStatus.UNKNOWN,
user_id=non_requester_user.id,
last_name=non_requester_user.last_name,
url="",
)
)

View File

@ -18,9 +18,7 @@ class SelfEvaluationFeedbackSerializer(serializers.ModelSerializer):
learning_unit_id = serializers.PrimaryKeyRelatedField(
read_only=True, source="learning_unit"
)
feedback_id = serializers.PrimaryKeyRelatedField(
read_only=True, source="course_completion_feedback"
)
feedback_id = serializers.PrimaryKeyRelatedField(read_only=True, source="id")
circle_name = serializers.SerializerMethodField()
title = serializers.CharField(source="learning_unit.title")

View File

@ -370,7 +370,7 @@ class SelfEvaluationFeedbackAPI(APITestCase):
feedback.feedback_assessment, CourseCompletionStatus.FAIL.value
)
def test_submit_self_evaluation_feedback(self):
def test_release_self_evaluation_feedback(self):
# GIVEN
learning_unit = create_learning_unit(course=self.course, circle=self.circle)
self_evaluation_feedback = create_self_evaluation_feedback(
@ -385,7 +385,7 @@ class SelfEvaluationFeedbackAPI(APITestCase):
# WHEN
response = self.client.put(
reverse(
"submit_self_evaluation_feedback", args=[self_evaluation_feedback.id]
"release_self_evaluation_feedback", args=[self_evaluation_feedback.id]
),
)

View File

@ -4,8 +4,8 @@ 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,
submit_provider_self_evaluation_feedback,
)
urlpatterns = [
@ -25,9 +25,9 @@ urlpatterns = [
name="get_self_evaluation_feedback_as_provider",
),
path(
"provider/feedback/<int:feedback_id>/submit",
submit_provider_self_evaluation_feedback,
name="submit_self_evaluation_feedback",
"provider/feedback/<int:feedback_id>/release",
release_provider_self_evaluation_feedback,
name="release_self_evaluation_feedback",
),
path(
"provider/feedback/<int:feedback_id>/add-assessment",

View File

@ -48,7 +48,7 @@ def start_self_evaluation_feedback(request, learning_unit_id):
@api_view(["PUT"])
@permission_classes([IsAuthenticated])
def submit_provider_self_evaluation_feedback(request, feedback_id):
def release_provider_self_evaluation_feedback(request, feedback_id):
feedback = get_object_or_404(
SelfEvaluationFeedback, id=feedback_id, feedback_provider_user=request.user
)
@ -56,6 +56,8 @@ def submit_provider_self_evaluation_feedback(request, feedback_id):
feedback.feedback_submitted = True
feedback.save()
# TODO: Create notification for feedback_requester_user
return Response({"success": True})