Add mentor components and basic styling

This commit is contained in:
Christian Cueni 2024-04-09 08:59:44 +02:00
parent 89fc3a8deb
commit eda9829b36
11 changed files with 232 additions and 25 deletions

View File

@ -1,9 +1,12 @@
<script setup lang="ts">
import { computed } from "vue";
import type { DashboardConfigType, WidgetType } from "@/services/dashboard";
import type { DashboardCourseConfigType, WidgetType } from "@/services/dashboard";
import LearningPathDiagram from "@/components/learningPath/LearningPathDiagram.vue";
import CompetenceSummary from "@/components/dashboard/CompetenceSummary.vue";
import AssignmentSummary from "@/components/dashboard/AssignmentSummary.vue";
import MentorOpenTasksCount from "@/components/dashboard/MentorOpenTasksCount.vue";
import MentorMenteeCount from "@/components/dashboard/MentorMenteeCount.vue";
import MentorCompetenceSummary from "@/components/dashboard/MentorCompetenceSummary.vue";
const mentorWidgets = [
"MentorTasksWidget",
@ -13,7 +16,7 @@ const mentorWidgets = [
const progressWidgets = ["CompetenceWidget", "CompetenceCertificateWidget"];
const props = defineProps<{
courseConfig: DashboardConfigType;
courseConfig: DashboardCourseConfigType;
}>();
const courseSlug = computed(() => props.courseConfig?.course_slug);
@ -34,9 +37,11 @@ function hasWidget(widget: WidgetType) {
<template>
<div v-if="courseConfig" class="mb-14 space-y-8">
<div class="flex flex-col space-y-7 bg-white p-6">
<h3>{{ courseName }}</h3>
<p>{{ courseConfig.role_key }}</p>
<div class="flex flex-col space-y-4 bg-white p-6">
<h3 class="mb-4 text-3xl">{{ courseName }}</h3>
<p>
<span class="rounded bg-gray-300 px-2 py-1">{{ courseConfig.role_key }}</span>
</p>
<LearningPathDiagram
v-if="
hasWidget('ProgressWidget') &&
@ -62,10 +67,19 @@ function hasWidget(widget: WidgetType) {
:course-id="courseConfig.course_id"
/>
</div>
<div v-if="numberOfMentorWidgets > 0" class="flex flex-col flex-wrap">
<div v-if="hasWidget('MentorTasksWidget')">MENTOR_TASKS_WIDGET</div>
<div v-if="hasWidget('MentorPersonWidget')">MENTOR_PERSON_WIDGET</div>
<div v-if="hasWidget('MentorCompetenceWidget')">MENTOR_COMPETENCE_WIDGET</div>
<div v-if="numberOfMentorWidgets > 0" class="flex flex-col flex-wrap md:flex-row">
<MentorOpenTasksCount
v-if="hasWidget('MentorTasksWidget')"
:course-id="courseConfig.course_id"
/>
<MentorMenteeCount
v-if="hasWidget('MentorPersonWidget')"
:course-id="courseConfig.course_id"
/>
<MentorCompetenceSummary
v-if="hasWidget('MentorCompetenceWidget')"
:course-id="courseConfig.course_id"
/>
</div>
</div>
</div>

View File

@ -0,0 +1,38 @@
<script setup lang="ts">
import { onMounted, ref, Ref } from "vue";
import { fetchMentorCompetenceSummary } from "@/services/dashboard";
import { AssignmentsStatisticsType } from "@/gql/graphql";
const props = defineProps<{
courseId: string;
}>();
const summary: Ref<AssignmentsStatisticsType | null> = ref(null);
onMounted(async () => {
summary.value = await fetchMentorCompetenceSummary(props.courseId);
console.log(summary.value);
});
</script>
<template>
<div v-if="summary" class="w-[325px]">
<div class="flex flex-row space-x-3 bg-white">
<div
class="flex h-[74px] items-center justify-center px-3 py-1 text-3xl font-bold"
>
<span>{{ summary.summary.total_passed }}</span>
</div>
<p class="ml-3 mt-0 leading-[74px]">{{ $t("Bestanden") }}</p>
</div>
<div class="flex flex-row space-x-3 bg-white pb-6">
<div
class="flex h-[74px] items-center justify-center px-3 py-1 text-3xl font-bold"
>
<span>{{ summary.summary.total_failed }}</span>
</div>
<p class="ml-3 mt-0 leading-[74px]">{{ $t("Nicht bestanden") }}</p>
</div>
<p>{{ $t("Details anschauen") }}</p>
</div>
</template>

View File

@ -0,0 +1,29 @@
<script setup lang="ts">
import { onMounted, ref, Ref } from "vue";
import { fetchMenteeCount } from "@/services/dashboard";
const props = defineProps<{
courseId: string;
}>();
const menteeCount: Ref<number> = ref(0);
onMounted(async () => {
const data = await fetchMenteeCount(props.courseId);
menteeCount.value = data?.mentee_count;
});
</script>
<template>
<div class="w-[325px]">
<div class="flex flex-row space-x-3 bg-white pb-6">
<div
class="flex h-[74px] items-center justify-center px-3 py-1 text-3xl font-bold"
>
<span>{{ menteeCount }}</span>
</div>
<p class="ml-3 mt-0 leading-[74px]">{{ $t("Personen, die du begleitest") }}</p>
</div>
<p>{{ $t("Details anschauen") }}</p>
</div>
</template>

View File

@ -0,0 +1,29 @@
<script setup lang="ts">
import { onMounted, ref, Ref } from "vue";
import { fetchOpenTasksCount } from "@/services/dashboard";
const props = defineProps<{
courseId: string;
}>();
const openTaskCount: Ref<number> = ref(0);
onMounted(async () => {
const data = await fetchOpenTasksCount(props.courseId);
openTaskCount.value = data?.open_task_count;
});
</script>
<template>
<div class="w-[325px]">
<div class="flex flex-row space-x-3 bg-white pb-6">
<div
class="flex h-[74px] w-[74px] items-center justify-center rounded-full border-2 border-green-500 px-3 py-1 text-3xl font-bold"
>
<span>{{ openTaskCount }}</span>
</div>
<p class="ml-3 mt-0 leading-[74px]">{{ $t("Elemente zu erledigen") }}</p>
</div>
<p>{{ $t("Details anschauen") }}</p>
</div>
</template>

View File

@ -25,6 +25,7 @@ const documents = {
"\n query dashboardProgress($courseId: ID!) {\n course_progress(course_id: $courseId) {\n _id\n course_id\n session_to_continue_id\n competence {\n _id\n total_count\n success_count\n fail_count\n }\n assignment {\n _id\n total_count\n points_max_count\n points_achieved_count\n }\n }\n }\n": types.DashboardProgressDocument,
"\n query dashboardCourseData($courseId: ID!) {\n course_progress(course_id: $courseId) {\n _id\n course_id\n session_to_continue_id\n }\n }\n": types.DashboardCourseDataDocument,
"\n query courseStatistics($courseId: ID!) {\n course_statistics(course_id: $courseId) {\n _id\n course_id\n course_title\n course_slug\n course_session_properties {\n _id\n sessions {\n id\n name\n }\n generations\n circles {\n id\n name\n }\n }\n course_session_selection_ids\n course_session_selection_metrics {\n _id\n session_count\n participant_count\n expert_count\n }\n attendance_day_presences {\n _id\n records {\n _id\n course_session_id\n generation\n circle_id\n due_date\n participants_present\n participants_total\n details_url\n }\n summary {\n _id\n days_completed\n participants_present\n }\n }\n feedback_responses {\n _id\n records {\n _id\n course_session_id\n generation\n circle_id\n experts\n satisfaction_average\n satisfaction_max\n details_url\n }\n summary {\n _id\n satisfaction_average\n satisfaction_max\n total_responses\n }\n }\n assignments {\n _id\n summary {\n _id\n completed_count\n average_passed\n }\n records {\n _id\n course_session_id\n course_session_assignment_id\n circle_id\n generation\n assignment_title\n assignment_type_translation_key\n details_url\n deadline\n metrics {\n _id\n passed_count\n failed_count\n unranked_count\n ranking_completed\n average_passed\n }\n }\n }\n competences {\n _id\n summary {\n _id\n success_total\n fail_total\n }\n records {\n _id\n course_session_id\n generation\n circle_id\n title\n success_count\n fail_count\n details_url\n }\n }\n }\n }\n": types.CourseStatisticsDocument,
"\n query mentorCourseStatistics($courseId: ID!) {\n mentor_course_statistics(course_id: $courseId) {\n _id\n assignments {\n _id\n summary {\n _id\n total_passed\n total_failed\n }\n }\n }\n }\n": types.MentorCourseStatisticsDocument,
"\n mutation SendFeedbackMutation(\n $courseSessionId: ID!\n $learningContentId: ID!\n $learningContentType: String!\n $data: GenericScalar!\n $submitted: Boolean\n ) {\n send_feedback(\n course_session_id: $courseSessionId\n learning_content_page_id: $learningContentId\n learning_content_type: $learningContentType\n data: $data\n submitted: $submitted\n ) {\n feedback_response {\n id\n data\n submitted\n }\n errors {\n field\n messages\n }\n }\n }\n": types.SendFeedbackMutationDocument,
};
@ -90,6 +91,10 @@ export function graphql(source: "\n query dashboardCourseData($courseId: ID!) {
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n query courseStatistics($courseId: ID!) {\n course_statistics(course_id: $courseId) {\n _id\n course_id\n course_title\n course_slug\n course_session_properties {\n _id\n sessions {\n id\n name\n }\n generations\n circles {\n id\n name\n }\n }\n course_session_selection_ids\n course_session_selection_metrics {\n _id\n session_count\n participant_count\n expert_count\n }\n attendance_day_presences {\n _id\n records {\n _id\n course_session_id\n generation\n circle_id\n due_date\n participants_present\n participants_total\n details_url\n }\n summary {\n _id\n days_completed\n participants_present\n }\n }\n feedback_responses {\n _id\n records {\n _id\n course_session_id\n generation\n circle_id\n experts\n satisfaction_average\n satisfaction_max\n details_url\n }\n summary {\n _id\n satisfaction_average\n satisfaction_max\n total_responses\n }\n }\n assignments {\n _id\n summary {\n _id\n completed_count\n average_passed\n }\n records {\n _id\n course_session_id\n course_session_assignment_id\n circle_id\n generation\n assignment_title\n assignment_type_translation_key\n details_url\n deadline\n metrics {\n _id\n passed_count\n failed_count\n unranked_count\n ranking_completed\n average_passed\n }\n }\n }\n competences {\n _id\n summary {\n _id\n success_total\n fail_total\n }\n records {\n _id\n course_session_id\n generation\n circle_id\n title\n success_count\n fail_count\n details_url\n }\n }\n }\n }\n"): (typeof documents)["\n query courseStatistics($courseId: ID!) {\n course_statistics(course_id: $courseId) {\n _id\n course_id\n course_title\n course_slug\n course_session_properties {\n _id\n sessions {\n id\n name\n }\n generations\n circles {\n id\n name\n }\n }\n course_session_selection_ids\n course_session_selection_metrics {\n _id\n session_count\n participant_count\n expert_count\n }\n attendance_day_presences {\n _id\n records {\n _id\n course_session_id\n generation\n circle_id\n due_date\n participants_present\n participants_total\n details_url\n }\n summary {\n _id\n days_completed\n participants_present\n }\n }\n feedback_responses {\n _id\n records {\n _id\n course_session_id\n generation\n circle_id\n experts\n satisfaction_average\n satisfaction_max\n details_url\n }\n summary {\n _id\n satisfaction_average\n satisfaction_max\n total_responses\n }\n }\n assignments {\n _id\n summary {\n _id\n completed_count\n average_passed\n }\n records {\n _id\n course_session_id\n course_session_assignment_id\n circle_id\n generation\n assignment_title\n assignment_type_translation_key\n details_url\n deadline\n metrics {\n _id\n passed_count\n failed_count\n unranked_count\n ranking_completed\n average_passed\n }\n }\n }\n competences {\n _id\n summary {\n _id\n success_total\n fail_total\n }\n records {\n _id\n course_session_id\n generation\n circle_id\n title\n success_count\n fail_count\n details_url\n }\n }\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n query mentorCourseStatistics($courseId: ID!) {\n mentor_course_statistics(course_id: $courseId) {\n _id\n assignments {\n _id\n summary {\n _id\n total_passed\n total_failed\n }\n }\n }\n }\n"): (typeof documents)["\n query mentorCourseStatistics($courseId: ID!) {\n mentor_course_statistics(course_id: $courseId) {\n _id\n assignments {\n _id\n summary {\n _id\n total_passed\n total_failed\n }\n }\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/

File diff suppressed because one or more lines are too long

View File

@ -452,3 +452,19 @@ export const DASHBOARD_COURSE_STATISTICS = graphql(`
}
}
`);
export const DASHBOARD_MENTOR_COMPETENCE_SUMMARY = graphql(`
query mentorCourseStatistics($courseId: ID!) {
mentor_course_statistics(course_id: $courseId) {
_id
assignments {
_id
summary {
_id
total_passed
total_failed
}
}
}
}
`);

View File

@ -65,10 +65,8 @@ function newDashboardConfigForId(id: string) {
@update:model-value="dashboardStore.switchAndLoadDashboardConfig"
></ItDropdownSelect>
</div>
<ul>
<li v-for="config in dashboardStore.dashboardConfigs" :key="config.id">
<p>{{ newDashboardConfigForId(config.id) }}</p>
<CoursePanel :course-config="newDashboardConfigForId(config.id)" />
</li>
</ul>

View File

@ -1,13 +1,14 @@
import { graphqlClient } from "@/graphql/client";
import {
DASHBOARD_CONFIG,
DASHBOARD_COURSE_DATA,
DASHBOARD_COURSE_SESSION_PROGRESS,
DASHBOARD_COURSE_STATISTICS,
DASHBOARD_MENTOR_COMPETENCE_SUMMARY,
} from "@/graphql/queries";
import { itGetCached } from "@/fetchHelpers";
import type {
AssignmentsStatisticsType,
CourseProgressType,
CourseStatisticsType,
DashboardConfigType,
@ -120,18 +121,18 @@ export const fetchDashboardConfig = async (): Promise<DashboardConfigType[] | nu
}
};
export const fetchCourseData = async (
export const fetchMentorCompetenceSummary = async (
courseId: string
): Promise<CourseProgressType | null> => {
): Promise<AssignmentsStatisticsType | null> => {
try {
const res = await graphqlClient.query(DASHBOARD_COURSE_DATA, {
const res = await graphqlClient.query(DASHBOARD_MENTOR_COMPETENCE_SUMMARY, {
courseId,
});
if (res.error) {
console.error("Error fetching data for course ID:", courseId, res.error);
}
return res.data?.course_progress || null;
return res.data?.mentor_course_statistics?.assignments || null;
} catch (error) {
console.error(`Error fetching data for course ID: ${courseId}`, error);
return null;
@ -145,3 +146,15 @@ export async function fetchDashboardPersons() {
export async function fetchDashboardConfigv2() {
return await itGetCached<DashboardCourseConfigType[]>("/api/dashboard/config/");
}
export async function fetchMenteeCount(courseId: string) {
return await itGetCached<{ mentee_count: number }>(
`/api/dashboard/course/${courseId}/mentees/`
);
}
export async function fetchOpenTasksCount(courseId: string) {
return await itGetCached<{ open_task_count: number }>(
`/api/dashboard/course/${courseId}/open_tasks/`
);
}

View File

@ -10,6 +10,9 @@ from django.views import defaults as default_views
from django.views.decorators.csrf import csrf_exempt
from django_ratelimit.exceptions import Ratelimited
from graphene_django.views import GraphQLView
from wagtail import urls as wagtail_urls
from wagtail.admin import urls as wagtailadmin_urls
from wagtail.documents import urls as media_library_urls
from vbv_lernwelt.api.directory import list_entities
from vbv_lernwelt.api.user import get_profile, me_user_view, post_avatar
@ -39,7 +42,8 @@ from vbv_lernwelt.course.views import (
request_course_completion_for_user,
)
from vbv_lernwelt.course_session.views import get_course_session_documents
from vbv_lernwelt.dashboard.views import get_dashboard_config, get_dashboard_persons
from vbv_lernwelt.dashboard.views import get_dashboard_config, get_dashboard_persons, get_mentee_count, \
get_mentor_open_tasks_count
from vbv_lernwelt.edoniq_test.views import (
export_students,
export_students_and_trainers,
@ -58,9 +62,6 @@ from vbv_lernwelt.importer.views import (
)
from vbv_lernwelt.media_files.views import user_image
from vbv_lernwelt.notify.views import email_notification_settings
from wagtail import urls as wagtail_urls
from wagtail.admin import urls as wagtailadmin_urls
from wagtail.documents import urls as media_library_urls
class SignedIntConverter(IntConverter):
@ -120,6 +121,9 @@ urlpatterns = [
# dashboard
path(r"api/dashboard/persons/", get_dashboard_persons, name="get_dashboard_persons"),
path(r"api/dashboard/config/", get_dashboard_config, name="get_dashboard_config"),
path(r"api/dashboard/course/<str:course_id>/mentees/", get_mentee_count, name="get_mentee_count"),
path(r"api/dashboard/course/<str:course_id>/open_tasks/", get_mentor_open_tasks_count,
name="get_mentor_open_tasks_count"),
# course
path(r"api/course/sessions/", get_course_sessions, name="get_course_sessions"),

View File

@ -6,11 +6,13 @@ from rest_framework.decorators import api_view
from rest_framework.exceptions import PermissionDenied
from rest_framework.response import Response
from vbv_lernwelt.assignment.models import AssignmentCompletion, AssignmentCompletionStatus
from vbv_lernwelt.core.models import User
from vbv_lernwelt.course.models import CourseSession, CourseSessionUser
from vbv_lernwelt.course.models import CourseSession, CourseSessionUser, CourseConfiguration
from vbv_lernwelt.course.views import logger
from vbv_lernwelt.course_session_group.models import CourseSessionGroup
from vbv_lernwelt.learning_mentor.models import LearningMentor
from vbv_lernwelt.self_evaluation_feedback.models import SelfEvaluationFeedback
class WidgetType(Enum):
@ -242,7 +244,7 @@ def get_widgets_for_course(
return widgets
def get_role_and_mentor_key(
def get_role_key_and_mentor(
course_sessions: List[CourseSessionWithRoles], is_uk: bool, is_vv: bool
) -> tuple[RoleKeyType, bool]:
roles = set()
@ -266,7 +268,7 @@ def get_role_and_mentor_key(
return role, is_mentor
def sort_course_sessions_by_course(
def collect_course_sessions_by_course(
course_sessions: List[CourseSessionWithRoles],
) -> dict:
course_sessions_by_course = {}
@ -298,11 +300,11 @@ def get_course_config(
course_sessions: List[CourseSessionWithRoles],
) -> List[CourseConfig]:
course_configs = []
cs_by_course = sort_course_sessions_by_course(course_sessions)
cs_by_course = collect_course_sessions_by_course(course_sessions)
for _id, cs_in_course in cs_by_course.items():
is_uk = cs_in_course[0].course.configuration.is_uk
is_vv = cs_in_course[0].course.configuration.is_vv
role_key, is_mentor = get_role_and_mentor_key(cs_in_course, is_uk, is_vv)
role_key, is_mentor = get_role_key_and_mentor(cs_in_course, is_uk, is_vv)
session_to_continue = get_newest_cs(cs_in_course)
course_configs.append(
CourseConfig(
@ -339,3 +341,54 @@ def get_dashboard_config(request):
except Exception as e:
logger.error(e, exc_info=True)
return Response({"error": str(e)}, status=404)
@api_view(["GET"])
def get_mentee_count(request, course_id: str):
try:
count = CourseSessionUser.objects.filter(
participants__mentor=request.user, course_session__course__id=course_id
).count()
return Response(
status=200,
data={"mentee_count": count},
)
except PermissionDenied as e:
raise e
except Exception as e:
logger.error(e, exc_info=True)
return Response({"error": str(e)}, status=404)
@api_view(["GET"])
def get_mentor_open_tasks_count(request, course_id: str):
try:
open_assigment_count = 0
open_feedback_count = 0
course_configuration = CourseConfiguration.objects.get(course_id=course_id)
if course_configuration.is_vv:
open_assigment_count = AssignmentCompletion.objects.filter(
course_session__course__id=course_id,
completion_status=AssignmentCompletionStatus.SUBMITTED.value,
evaluation_user=request.user, # noqa
assignment_user__coursesessionuser__participants__mentor=request.user,
).count()
open_feedback_count = SelfEvaluationFeedback.objects.filter(
feedback_provider_user=request.user, # noqa
feedback_requester_user__coursesessionuser__participants__mentor=request.user,
feedback_submitted=False,
).count()
return Response(
status=200,
data={"open_task_count": open_assigment_count + open_feedback_count},
)
except PermissionDenied as e:
raise e
except Exception as e:
logger.error(e, exc_info=True)
return Response({"error": str(e)}, status=404)