feat: feedback received screen

This commit is contained in:
Livio Bieri 2024-01-25 13:39:13 +01:00
parent bc78fe2533
commit 654ccb0d47
11 changed files with 374 additions and 177 deletions

View File

@ -0,0 +1,79 @@
<script setup lang="ts">
import { computed } from "vue";
import type { FeedbackRequest } from "@/services/selfEvaluationFeedback";
import { t } from "i18next";
const props = defineProps<{
feedback: FeedbackRequest;
}>();
console.log(props.feedback);
const feedbackProviderAvatar = computed(() => {
return props.feedback.feedback_provider_user.avatar_url;
});
const feedbackProviderName = computed(() => {
return `${props.feedback.feedback_provider_user.first_name} ${props.feedback.feedback_provider_user.last_name}`;
});
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";
}
};
const getCaption = (assessment: "FAIL" | "SUCCESS" | "UNKNOWN") => {
switch (assessment) {
case "SUCCESS":
return t("selfEvaluation.yes");
case "FAIL":
return t("selfEvaluation.no");
default:
return t("a.Nicht bewertet");
}
};
</script>
<template>
<div
v-for="criteria in props.feedback.criteria"
:key="criteria.course_completion_id"
class="mb-10"
>
<span>{{ criteria.title }}</span>
<div class="mt-3 grid grid-cols-2 border-2 border-gray-200">
<!-- Feedback requester assessment -->
<div class="flex h-12 items-center pl-4">
<b>{{ $t("a.Deine Selbsteinschätzung") }}</b>
</div>
<div class="flex items-center justify-start space-x-2 bg-white">
<component :is="getSmiley(criteria.self_assessment)" class="h-6 w-6" />
<span>{{ getCaption(criteria.self_assessment) }}</span>
</div>
<!-- Feedback provider assessment -->
<div class="flex h-12 items-center bg-gray-200 pl-4">
<b>
{{
$t("a.Fremdeinschätzung von FEEDBACK_PROVIDER_NAME", {
FEEDBACK_PROVIDER_NAME: feedbackProviderName,
})
}}
</b>
<img class="ml-2 h-7 w-7 rounded-full" :src="feedbackProviderAvatar" />
</div>
<div class="flex items-center justify-start space-x-2 bg-gray-200">
<component :is="getSmiley(criteria.feedback_assessment)" class="h-6 w-6" />
<span>{{ getCaption(criteria.feedback_assessment) }}</span>
</div>
</div>
</div>
</template>
<style scoped></style>

View File

@ -0,0 +1,41 @@
<script setup lang="ts">
import type { LearningUnit, LearningUnitPerformanceCriteria } from "@/types";
defineProps<{
learningUnit: LearningUnit;
criteria: LearningUnitPerformanceCriteria[];
showEditLink: boolean;
}>();
</script>
<template>
<div
v-for="(completion, index) in criteria"
:key="completion.id"
class="flex flex-col space-y-4 border-t border-gray-400 py-8"
>
<div class="flex justify-between">
<b>{{ completion.title }}</b>
<router-link
v-if="showEditLink"
:to="`${learningUnit.evaluate_url}?step=${index}`"
class="underline"
>
{{ $t("a.Bearbeiten") }}
</router-link>
</div>
<div
v-if="completion.completion_status == 'SUCCESS'"
class="flex flex-row items-center space-x-2"
>
<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">
<it-icon-smiley-thinking class="h-6 w-6" />
<span>{{ $t("selfEvaluation.no") }}</span>
</div>
</div>
</template>
<style scoped></style>

View File

@ -0,0 +1,26 @@
<script setup lang="ts">
defineProps<{
feedbackMentorName: string;
}>();
</script>
<template>
<div class="flex space-x-2 bg-green-200 p-4">
<it-icon-check class="it-icon h-6 w-6 text-green-700" />
<div>
{{
$t("a.Du hast deine Selbsteinschätzung erfolgreich mit FULL_NAME geteilt.", {
FULL_NAME: feedbackMentorName,
})
}}
</div>
</div>
<div class="pt-6">
{{
$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 }
)
}}
</div>
</template>

View File

@ -28,8 +28,8 @@ import type {
import { useQuery } from "@urql/vue";
import orderBy from "lodash/orderBy";
import log from "loglevel";
import type { ComputedRef, Ref } from "vue";
import { computed, onMounted, ref, toValue, watchEffect } from "vue";
import type { ComputedRef } from "vue";
import { computed, onMounted, ref, watchEffect } from "vue";
export function useCurrentCourseSession() {
/**
@ -487,34 +487,3 @@ export function useLearningMentors() {
loading,
};
}
export function useSelfEvaluationFeedback(learningUnitId: Ref<string> | string) {
const feedback = ref({});
const loading = ref(false);
const exists = ref(false);
const url = `/api/self-evaluation-feedback/requester/${toValue(
learningUnitId
)}/feedback`;
const fetchSelfEvaluationFeedback = async () => {
loading.value = true;
const { data, statusCode } = await useCSRFFetch(url).json();
loading.value = false;
if (statusCode.value === 404) {
exists.value = false;
return;
} else {
exists.value = true;
feedback.value = data.value;
}
};
onMounted(fetchSelfEvaluationFeedback);
return {
feedback,
exists,
loading,
};
}

View File

@ -15,24 +15,16 @@ import SelfEvaluationSubmit from "@/pages/learningPath/selfEvaluationPage/SelfEv
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 learningUnitHasFeedbackPage = computed(
() => props.learningUnit?.feedback_user !== "NO_FEEDBACK"
);
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;
@ -41,6 +33,13 @@ const numPages = computed(() => {
}
});
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(

View File

@ -1,25 +1,29 @@
<script setup lang="ts">
import type { LearningUnit, LearningUnitPerformanceCriteria } from "@/types";
import { useLearningMentors, useSelfEvaluationFeedback } from "@/composables";
import { useLearningMentors } from "@/composables";
import { computed, ref } from "vue";
import ItButton from "@/components/ui/ItButton.vue";
import NoMentorInformationPanel from "@/components/mentor/NoMentorInformationPanel.vue";
import { useCSRFFetch } from "@/fetchHelpers";
import { useSelfEvaluationFeedback } from "@/services/selfEvaluationFeedback";
import FeedbackRequestedInformationPanel from "@/components/selfEvaluationFeedback/FeedbackRequestedInformationPanel.vue";
import FeedbackReceived from "@/components/selfEvaluationFeedback/FeedbackReceived.vue";
import FeedbackRequested from "@/components/selfEvaluationFeedback/FeedbackRequested.vue";
const props = defineProps<{
learningUnit: LearningUnit;
criteria: LearningUnitPerformanceCriteria[];
}>();
const {
feedback: storedFeedback,
exists: isStoredFeedbackAvailable,
loading: isStoredFeedbackLoading,
} = useSelfEvaluationFeedback(props.learningUnit.id);
const selfEvaluationFeedback = useSelfEvaluationFeedback(props.learningUnit.id);
const storedFeedback = computed(() => selfEvaluationFeedback.feedback.value);
const isStoredFeedbackLoading = computed(() => selfEvaluationFeedback.loading.value);
// if no feedback is stored "current session" state management (mentor selection etc.)
const learningMentors = useLearningMentors();
const mentors = computed(() => learningMentors.learningMentors.value);
const isMentorsLoading = computed(() => learningMentors.loading.value);
// if no feedback is stored "current session"
// state management (mentor selection etc.)
const { learningMentors, loading: isLearningMentorsLoading } = useLearningMentors();
const isCurrentSessionFeedbackRequested = ref(false);
const currentSessionRequestedMentor = ref();
@ -27,34 +31,43 @@ const VisualState = {
LOADING: "LOADING",
NO_MENTOR: "NO_MENTOR",
HAS_REQUESTED_FEEDBACK: "HAS_REQUESTED_FEEDBACK",
HAS_RECEIVED_FEEDBACK: "HAS_RECEIVED_FEEDBACK",
HAS_NOT_REQUESTED_FEEDBACK: "HAS_NOT_REQUESTED_FEEDBACK",
};
const currentVisualState = computed(() => {
if (isLearningMentorsLoading.value || isStoredFeedbackLoading.value) {
if (isMentorsLoading.value || isStoredFeedbackLoading.value) {
return VisualState.LOADING;
} else if (learningMentors.value.length == 0) {
} else if (mentors.value.length == 0) {
return VisualState.NO_MENTOR;
} else if (
isStoredFeedbackAvailable.value ||
isCurrentSessionFeedbackRequested.value
) {
} else if (isCurrentSessionFeedbackRequested.value) {
return VisualState.HAS_REQUESTED_FEEDBACK;
} else if (storedFeedback.value && !storedFeedback.value.feedback_submitted) {
return VisualState.HAS_REQUESTED_FEEDBACK;
} else if (storedFeedback.value && storedFeedback.value.feedback_submitted) {
return VisualState.HAS_RECEIVED_FEEDBACK;
} else {
return VisualState.HAS_NOT_REQUESTED_FEEDBACK;
}
});
const feedbackMentorName = computed(() => {
const mentor = isStoredFeedbackAvailable.value
? (storedFeedback.value as any).feedback_provider_user
: isCurrentSessionFeedbackRequested.value
? currentSessionRequestedMentor.value
: null;
return mentor ? `${mentor.first_name} ${mentor.last_name}` : "";
let mentor = null;
if (storedFeedback.value) {
mentor = storedFeedback.value.feedback_provider_user;
} else if (isCurrentSessionFeedbackRequested.value) {
mentor = currentSessionRequestedMentor.value;
}
if (mentor) {
return `${mentor.first_name} ${mentor.last_name}`;
} else {
return "";
}
});
const onSubmitForMentorFeedback = async () => {
const onRequestFeedback = async () => {
const url = `/api/self-evaluation-feedback/requester/${props.learningUnit.id}/feedback/start`;
const { error } = await useCSRFFetch(url).post({
feedback_provider_user_id: currentSessionRequestedMentor.value.id,
@ -66,104 +79,77 @@ const onSubmitForMentorFeedback = async () => {
</script>
<template>
<div v-if="currentVisualState != VisualState.LOADING" class="mb-10 w-full pt-8">
<div class="w-full border border-gray-400">
<div class="m-6 space-y-6">
<h3 class="heading-3">
{{ $t("a.Selbsteinschätzung teilen") }}
</h3>
<div v-if="currentVisualState == VisualState.NO_MENTOR">
<NoMentorInformationPanel />
</div>
<div v-if="currentVisualState == VisualState.HAS_REQUESTED_FEEDBACK">
<div class="flex space-x-2 bg-green-200 p-4">
<it-icon-check class="it-icon h-6 w-6 text-green-700" />
<div>
<div v-if="currentVisualState != VisualState.LOADING">
<div class="mb-10 w-full pt-8">
<div
v-if="currentVisualState != VisualState.HAS_RECEIVED_FEEDBACK"
class="w-full border border-gray-400"
>
<div class="m-6 space-y-6">
<h3 class="heading-3">
{{ $t("a.Selbsteinschätzung teilen") }}
</h3>
<NoMentorInformationPanel
v-if="currentVisualState == VisualState.NO_MENTOR"
/>
<FeedbackRequestedInformationPanel
v-if="currentVisualState == VisualState.HAS_REQUESTED_FEEDBACK"
:feedback-mentor-name="feedbackMentorName"
/>
<div v-else-if="currentVisualState == VisualState.HAS_NOT_REQUESTED_FEEDBACK">
<p>
{{
$t(
"a.Du hast deine Selbsteinschätzung erfolgreich mit FULL_NAME geteilt.",
{
FULL_NAME: feedbackMentorName,
}
"a.Du kannst deine Selbsteinschätzung mit deiner Lernbegleitung teilen, damit sie eine Fremdeinschätzung vornimmt."
)
}}
</div>
</div>
<div class="pt-6">
{{
$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 }
)
}}
</div>
</div>
<div v-else-if="currentVisualState == VisualState.HAS_NOT_REQUESTED_FEEDBACK">
<p>
{{
$t(
"a.Du kannst deine Selbsteinschätzung mit deiner Lernbegleitung teilen, damit sie eine Fremdeinschätzung vornimmt."
)
}}
</p>
<select
v-model="currentSessionRequestedMentor"
data-cy="select-learning-mentor"
>
<option
v-for="learningMentor in learningMentors"
:key="learningMentor.id"
:value="learningMentor.mentor"
</p>
<select
v-model="currentSessionRequestedMentor"
data-cy="select-learning-mentor"
>
{{ learningMentor.mentor.first_name }}
{{ learningMentor.mentor.last_name }}
</option>
</select>
<ItButton
class="mt-6"
variant="primary"
size="large"
:disabled="!currentSessionRequestedMentor"
data-cy="submit-assignment"
@click="onSubmitForMentorFeedback"
>
<p v-if="!currentSessionRequestedMentor">
{{ $t("a.Selbsteinschätzung teilen") }}
</p>
<p v-else>
{{
$t("a.Selbsteinschätzung mit MENTOR_NAME teilen", {
MENTOR_NAME: feedbackMentorName,
})
}}
</p>
</ItButton>
<option
v-for="learningMentor in mentors"
:key="learningMentor.id"
:value="learningMentor.mentor"
>
{{ learningMentor.mentor.first_name }}
{{ learningMentor.mentor.last_name }}
</option>
</select>
<ItButton
class="mt-6"
variant="primary"
size="large"
:disabled="!currentSessionRequestedMentor"
data-cy="submit-assignment"
@click="onRequestFeedback"
>
<p v-if="!currentSessionRequestedMentor">
{{ $t("a.Selbsteinschätzung teilen") }}
</p>
<p v-else>
{{
$t("a.Selbsteinschätzung mit MENTOR_NAME teilen", {
MENTOR_NAME: feedbackMentorName,
})
}}
</p>
</ItButton>
</div>
</div>
</div>
</div>
</div>
<div
v-for="(completion, index) in criteria"
:key="completion.id"
class="flex flex-col space-y-4 border-t border-gray-400 py-8"
>
<div class="flex justify-between">
<b>{{ completion.title }}</b>
<router-link :to="`${learningUnit.evaluate_url}?step=${index}`" class="underline">
{{ $t("a.Bearbeiten") }}
</router-link>
</div>
<div
v-if="completion.completion_status == 'SUCCESS'"
class="flex flex-row items-center space-x-2"
>
<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">
<it-icon-smiley-thinking class="h-6 w-6" />
<span>{{ $t("selfEvaluation.no") }}</span>
</div>
<FeedbackReceived
v-if="currentVisualState == VisualState.HAS_RECEIVED_FEEDBACK && storedFeedback"
:feedback="storedFeedback"
/>
<FeedbackRequested
v-else
:criteria="props.criteria"
:learning-unit="props.learningUnit"
:show-edit-link="currentVisualState != VisualState.HAS_REQUESTED_FEEDBACK"
/>
</div>
</template>

View File

@ -0,0 +1,60 @@
import { useCSRFFetch } from "@/fetchHelpers";
import type { User } from "@/types";
import type { Ref } from "vue";
import { computed, onMounted, ref, toValue } from "vue";
export interface FeedbackRequest {
learning_unit_id: number;
circle_name: string;
// submitted => provider submitted (released) his/her feedback
feedback_submitted: boolean;
feedback_requester_user: User;
feedback_provider_user: User;
criteria: Criterion[];
}
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) {
const feedback = ref<FeedbackRequest>();
const loading = ref(false);
const error = ref();
const url = computed(
() => `/api/self-evaluation-feedback/requester/${toValue(learningUnitId)}/feedback`
);
const fetchSelfEvaluationFeedback = async () => {
feedback.value = undefined;
error.value = undefined;
loading.value = true;
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;
}
};
onMounted(fetchSelfEvaluationFeedback);
return {
feedback,
error,
loading,
};
}

View File

@ -589,3 +589,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;
};

View File

@ -4,20 +4,33 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('assignment', '0011_assignment_solution_sample'),
("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),
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?'),
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?",
),
),
]

View File

@ -346,11 +346,11 @@ def create_default_users(default_password="test"):
_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_url="",
avatar_url="/static/avatars/uk1.patrizia.huggel.jpg",
)

View File

@ -1,25 +1,36 @@
# Generated by Django 3.2.20 on 2024-01-24 09:04
from django.db import migrations, models
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'),
("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'),
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'),
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",
),
),
]