feat: mentor feedback
This commit is contained in:
parent
abb371d4a0
commit
f228f9ee47
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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="",
|
||||
)
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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})
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue