WIP: Use REST endpoint

This commit is contained in:
Christian Cueni 2024-04-08 12:47:55 +02:00
parent 5ba319e524
commit 4a982d8af2
8 changed files with 258 additions and 69 deletions

View File

@ -1,80 +1,71 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, onMounted, ref } from "vue"; import { computed } from "vue";
import type { CourseProgressType, WidgetType } from "@/gql/graphql"; import type { DashboardConfigType, WidgetType } from "@/services/dashboard";
import { DashboardConfigType } from "@/gql/graphql";
import { fetchCourseData } from "@/services/dashboard";
import LearningPathDiagram from "@/components/learningPath/LearningPathDiagram.vue"; import LearningPathDiagram from "@/components/learningPath/LearningPathDiagram.vue";
import CompetenceSummary from "@/components/dashboard/CompetenceSummary.vue"; import CompetenceSummary from "@/components/dashboard/CompetenceSummary.vue";
import AssignmentSummary from "@/components/dashboard/AssignmentSummary.vue"; import AssignmentSummary from "@/components/dashboard/AssignmentSummary.vue";
const mentorWidgets = [ const mentorWidgets = [
"MENTOR_TASKS_WIDGET", "MentorTasksWidget",
"MENTOR_PERSON_WIDGET", "MentorPersonWidget",
"MENTOR_COMPETENCE_WIDGET", "MentorCompetenceWidget",
]; ];
const progressWidgets = ["COMPETENCE_WIDGET", "COMPETENCE_CERTIFICATE_WIDGET"]; const progressWidgets = ["CompetenceWidget", "CompetenceCertificateWidget"];
const props = defineProps<{ const props = defineProps<{
courseConfig: DashboardConfigType; courseConfig: DashboardConfigType;
}>(); }>();
const isLoading = ref(true); const courseSlug = computed(() => props.courseConfig?.course_slug);
const courseName = computed(() => props.courseConfig?.course_title);
const data = ref<CourseProgressType | null>(null);
const courseSlug = computed(() => props.courseConfig?.slug);
const courseName = computed(() => props.courseConfig?.name);
const numberOfMentorWidgets = computed(() => { const numberOfMentorWidgets = computed(() => {
return data.value?.ui_config.widgets.filter((widget) => return props.courseConfig.widgets.filter((widget) => mentorWidgets.includes(widget))
mentorWidgets.includes(widget) .length;
).length;
}); });
const numberOfProgressWidgets = computed(() => { const numberOfProgressWidgets = computed(() => {
return data.value?.ui_config.widgets.filter((widget) => return props.courseConfig.widgets.filter((widget) => progressWidgets.includes(widget))
progressWidgets.includes(widget) .length;
).length;
}); });
function hasWidget(widget: WidgetType) { function hasWidget(widget: WidgetType) {
return data.value?.ui_config.widgets.includes(widget); return props.courseConfig.widgets.includes(widget);
} }
onMounted(async () => {
data.value = await fetchCourseData(props.courseConfig.id);
isLoading.value = false;
});
</script> </script>
<template> <template>
<div v-if="!isLoading && courseConfig" class="mb-14 space-y-8"> <div v-if="courseConfig" class="mb-14 space-y-8">
<div class="flex flex-col space-y-7 bg-white p-6"> <div class="flex flex-col space-y-7 bg-white p-6">
<h3>{{ courseName }}</h3> <h3>{{ courseName }}</h3>
<p>{{ data.ui_config.role_key }}</p> <p>{{ courseConfig.role_key }}</p>
<LearningPathDiagram <LearningPathDiagram
v-if="hasWidget('PROGRESS_WIDGET') && data.session_to_continue_id && courseSlug" v-if="
hasWidget('ProgressWidget') &&
courseConfig.session_to_continue_id &&
courseSlug
"
:key="courseSlug" :key="courseSlug"
:course-slug="courseSlug" :course-slug="courseSlug"
:course-session-id="data.session_to_continue_id" :course-session-id="courseConfig.session_to_continue_id"
diagram-type="horizontal" diagram-type="horizontal"
></LearningPathDiagram> ></LearningPathDiagram>
<div v-if="numberOfProgressWidgets" class="flex flex-col flex-wrap"> <div v-if="numberOfProgressWidgets" class="flex flex-col flex-wrap">
<CompetenceSummary <CompetenceSummary
v-if="hasWidget('COMPETENCE_WIDGET')" v-if="hasWidget('CompetenceWidget')"
:course-slug="courseSlug" :course-slug="courseSlug"
:session-to-continue-id="data.session_to_continue_id" :session-to-continue-id="courseConfig.session_to_continue_id"
:course-id="courseConfig.id" :course-id="courseConfig.course_id"
></CompetenceSummary> ></CompetenceSummary>
<AssignmentSummary <AssignmentSummary
v-if="hasWidget('COMPETENCE_CERTIFICATE_WIDGET')" v-if="hasWidget('CompetenceCertificateWidget')"
:course-slug="courseSlug" :course-slug="courseSlug"
:session-to-continue-id="data.session_to_continue_id" :session-to-continue-id="courseConfig.session_to_continue_id"
:course-id="courseConfig.id" :course-id="courseConfig.course_id"
/> />
</div> </div>
<div v-if="numberOfMentorWidgets > 0" class="flex flex-col flex-wrap"> <div v-if="numberOfMentorWidgets > 0" class="flex flex-col flex-wrap">
<div v-if="hasWidget('MENTOR_TASKS_WIDGET')">MENTOR_TASKS_WIDGET</div> <div v-if="hasWidget('MentorTasksWidget')">MENTOR_TASKS_WIDGET</div>
<div v-if="hasWidget('MENTOR_PERSON_WIDGET')">MENTOR_PERSON_WIDGET</div> <div v-if="hasWidget('MentorPersonWidget')">MENTOR_PERSON_WIDGET</div>
<div v-if="hasWidget('MENTOR_COMPETENCE_WIDGET')">MENTOR_COMPETENCE_WIDGET</div> <div v-if="hasWidget('MentorCompetenceWidget')">MENTOR_COMPETENCE_WIDGET</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import type { Component } from "vue"; import type { Component, Ref } from "vue";
import { onMounted } from "vue"; import { onMounted, ref } from "vue";
import StatisticPage from "@/pages/dashboard/StatisticPage.vue"; import StatisticPage from "@/pages/dashboard/StatisticPage.vue";
import ProgressPage from "@/pages/dashboard/ProgressPage.vue"; import ProgressPage from "@/pages/dashboard/ProgressPage.vue";
import SimpleDates from "@/components/dashboard/SimpleDates.vue"; import SimpleDates from "@/components/dashboard/SimpleDates.vue";
@ -13,6 +13,7 @@ import CourseDetailDates from "@/components/dashboard/CourseDetailDates.vue";
import NoCourseSession from "@/components/dashboard/NoCourseSession.vue"; import NoCourseSession from "@/components/dashboard/NoCourseSession.vue";
import MentorPage from "@/pages/dashboard/MentorPage.vue"; import MentorPage from "@/pages/dashboard/MentorPage.vue";
import CoursePanel from "@/components/dashboard/CoursePanel.vue"; import CoursePanel from "@/components/dashboard/CoursePanel.vue";
import { DashboardConfigType, fetchDashboardConfigv2 } from "@/services/dashboard";
const dashboardStore = useDashboardStore(); const dashboardStore = useDashboardStore();
@ -29,7 +30,16 @@ const boards: Record<DashboardType, DashboardPage> = {
PRAXISBILDNER_DASHBOARD: { main: CoursePanel, aside: SimpleDates }, PRAXISBILDNER_DASHBOARD: { main: CoursePanel, aside: SimpleDates },
}; };
onMounted(dashboardStore.loadDashboardDetails); const dashboardConfigv2: Ref<DashboardConfigType[]> = ref([]);
onMounted(async () => {
dashboardConfigv2.value = await fetchDashboardConfigv2();
await dashboardStore.loadDashboardDetails();
});
function newDashboardConfigForId(id: string) {
return dashboardConfigv2.value.find((config) => config.course_id == +id);
}
</script> </script>
<template> <template>
@ -54,8 +64,8 @@ onMounted(dashboardStore.loadDashboardDetails);
<ul> <ul>
<li v-for="config in dashboardStore.dashboardConfigs" :key="config.id"> <li v-for="config in dashboardStore.dashboardConfigs" :key="config.id">
<p>{{ config }}</p> <p>{{ newDashboardConfigForId(config.id) }}</p>
<CoursePanel :course-config="config" /> <CoursePanel :course-config="newDashboardConfigForId(config.id)" />
</li> </li>
</ul> </ul>
<!-- keep until we unify the dashboard --> <!-- keep until we unify the dashboard -->

View File

@ -20,6 +20,21 @@ export type DashboardPersonRoleType =
| "LEARNING_MENTOR" | "LEARNING_MENTOR"
| "LEARNING_MENTEE"; | "LEARNING_MENTEE";
export type DashboardRoleKeyType =
| "Supervisor"
| "Expert"
| "Member"
| "MentorUK"
| "MentorVV";
export type WidgetType =
| "ProgressWidget"
| "CompetenceWidget"
| "MentorTasksWidget"
| "MentorPersonWidget"
| "MentorCompetenceWidget"
| "CompetenceCertificateWidget";
export type DashboardPersonCourseSessionType = { export type DashboardPersonCourseSessionType = {
id: number; id: number;
session_title: string; session_title: string;
@ -40,6 +55,19 @@ export type DashboardPersonType = {
course_sessions: DashboardPersonCourseSessionType[]; course_sessions: DashboardPersonCourseSessionType[];
}; };
export type DashboardConfigType = {
course_id: string;
course_slug: string;
course_title: string;
role_key: DashboardRoleKeyType;
is_uk: boolean;
is_vv: boolean;
is_mentor: boolean;
widgets: WidgetType[];
has_preview: boolean;
session_to_continue_id: string;
};
export const fetchStatisticData = async ( export const fetchStatisticData = async (
courseId: string courseId: string
): Promise<CourseStatisticsType | null> => { ): Promise<CourseStatisticsType | null> => {
@ -113,3 +141,7 @@ export const fetchCourseData = async (
export async function fetchDashboardPersons() { export async function fetchDashboardPersons() {
return await itGetCached<DashboardPersonType[]>("/api/dashboard/persons/"); return await itGetCached<DashboardPersonType[]>("/api/dashboard/persons/");
} }
export async function fetchDashboardConfigv2() {
return await itGetCached<DashboardConfigType[]>("/api/dashboard/config/");
}

View File

@ -39,7 +39,7 @@ from vbv_lernwelt.course.views import (
request_course_completion_for_user, request_course_completion_for_user,
) )
from vbv_lernwelt.course_session.views import get_course_session_documents from vbv_lernwelt.course_session.views import get_course_session_documents
from vbv_lernwelt.dashboard.views import get_dashboard_persons from vbv_lernwelt.dashboard.views import get_dashboard_config, get_dashboard_persons
from vbv_lernwelt.edoniq_test.views import ( from vbv_lernwelt.edoniq_test.views import (
export_students, export_students,
export_students_and_trainers, export_students_and_trainers,
@ -116,9 +116,10 @@ urlpatterns = [
# notify # notify
re_path(r"api/notify/email_notification_settings/$", email_notification_settings, re_path(r"api/notify/email_notification_settings/$", email_notification_settings,
name='email_notification_settings'), name='email_notification_settings'),
# dashboard # dashboard
path(r"api/dashboard/persons/", get_dashboard_persons, name="get_dashboard_persons"), path(r"api/dashboard/persons/", get_dashboard_persons, name="get_dashboard_persons"),
path(r"api/dashboard/config/", get_dashboard_config, name="get_dashboard_config"),
# course # course
path(r"api/course/sessions/", get_course_sessions, name="get_course_sessions"), path(r"api/course/sessions/", get_course_sessions, name="get_course_sessions"),

View File

@ -75,8 +75,9 @@ class DashboardQuery(graphene.ObjectType):
mentees_ids = set() mentees_ids = set()
course_session_ids = set() course_session_ids = set()
mentees = CourseSessionUser.objects.filter(participants__mentor=user, mentees = CourseSessionUser.objects.filter(
course_session__course=course).values_list("user", "course_session") participants__mentor=user, course_session__course=course
).values_list("user", "course_session")
for user_id, course_session_id in mentees: for user_id, course_session_id in mentees:
mentees_ids.add(user_id) mentees_ids.add(user_id)
course_session_ids.add(course_session_id) course_session_ids.add(course_session_id)

View File

@ -74,8 +74,9 @@ def create_assignment_summary(course_id, metrics) -> AssignmentStatisticsSummary
def get_assignment_completion_metrics( def get_assignment_completion_metrics(
course_session: CourseSession, assignment: vbv_lernwelt.assignment.models.Assignment, course_session: CourseSession,
user_selection_ids: List[str] | None assignment: vbv_lernwelt.assignment.models.Assignment,
user_selection_ids: List[str] | None,
) -> AssignmentCompletionMetricsType: ) -> AssignmentCompletionMetricsType:
course_session_users = CourseSessionUser.objects.filter( course_session_users = CourseSessionUser.objects.filter(
course_session=course_session, course_session=course_session,
@ -112,7 +113,7 @@ def get_assignment_completion_metrics(
def create_record( def create_record(
course_session_assignment: CourseSessionAssignment | CourseSessionEdoniqTest, course_session_assignment: CourseSessionAssignment | CourseSessionEdoniqTest,
user_selection_ids: List[str] | None user_selection_ids: List[str] | None,
) -> AssignmentStatisticsRecordType: ) -> AssignmentStatisticsRecordType:
if isinstance(course_session_assignment, CourseSessionAssignment): if isinstance(course_session_assignment, CourseSessionAssignment):
due_date = course_session_assignment.submission_deadline due_date = course_session_assignment.submission_deadline
@ -160,14 +161,18 @@ def assignments(
], ],
learning_content__content_assignment__competence_certificate__isnull=False, learning_content__content_assignment__competence_certificate__isnull=False,
): ):
record = create_record(course_session_assignment=csa, user_selection_ids=user_selection_ids) record = create_record(
course_session_assignment=csa, user_selection_ids=user_selection_ids
)
records.append(record) records.append(record)
for cset in CourseSessionEdoniqTest.objects.filter( for cset in CourseSessionEdoniqTest.objects.filter(
course_session=course_session, course_session=course_session,
learning_content__content_assignment__competence_certificate__isnull=False, learning_content__content_assignment__competence_certificate__isnull=False,
): ):
record = create_record(course_session_assignment=cset, user_selection_ids=user_selection_ids) record = create_record(
course_session_assignment=cset, user_selection_ids=user_selection_ids
)
records.append(record) records.append(record)
return AssignmentsStatisticsType( return AssignmentsStatisticsType(

View File

@ -241,8 +241,11 @@ class CourseStatisticsType(graphene.ObjectType):
) )
def resolve_competences(root, info) -> CompetencesStatisticsType: def resolve_competences(root, info) -> CompetencesStatisticsType:
user_selection_ids = [str(user) for user in user_selection_ids = (
root.user_selection_ids] if root.user_selection_ids else None # noqa [str(user) for user in root.user_selection_ids]
if root.user_selection_ids
else None
) # noqa
records, success_total, fail_total = competences( records, success_total, fail_total = competences(
course_slug=str(root.course_slug), course_slug=str(root.course_slug),
course_session_selection_ids=[ course_session_selection_ids=[
@ -261,8 +264,11 @@ class CourseStatisticsType(graphene.ObjectType):
) )
def resolve_assignments(root, info) -> AssignmentsStatisticsType: def resolve_assignments(root, info) -> AssignmentsStatisticsType:
user_selection_ids = [str(user) for user in user_selection_ids = (
root.user_selection_ids] if root.user_selection_ids else None # noqa [str(user) for user in root.user_selection_ids]
if root.user_selection_ids
else None
) # noqa
return assignments( return assignments(
course_id=root.course_id, course_id=root.course_id,
course_session_selection_ids=root.course_session_selection_ids, course_session_selection_ids=root.course_session_selection_ids,
@ -381,7 +387,11 @@ def get_ui_config_for_course(course: Course, user: User) -> UIConfigType:
# mentors # mentors
if course.id in mentor_course_ids: if course.id in mentor_course_ids:
if not role_key: if not role_key:
role_key = RoleKeyType.MENTOR_UK if course.id in UK_COURSE_IDS else RoleKeyType.MENTOR_VV role_key = (
RoleKeyType.MENTOR_UK
if course.id in UK_COURSE_IDS
else RoleKeyType.MENTOR_VV
)
has_preview = True has_preview = True
widgets.append(WidgetType.MENTOR_TASKS_WIDGET) widgets.append(WidgetType.MENTOR_TASKS_WIDGET)
widgets.append(WidgetType.MENTOR_PERSON_WIDGET) widgets.append(WidgetType.MENTOR_PERSON_WIDGET)

View File

@ -1,4 +1,4 @@
from dataclasses import dataclass from dataclasses import asdict, dataclass
from typing import List, Set from typing import List, Set
from rest_framework.decorators import api_view from rest_framework.decorators import api_view
@ -9,6 +9,7 @@ from vbv_lernwelt.core.models import User
from vbv_lernwelt.course.models import CourseSession, CourseSessionUser from vbv_lernwelt.course.models import CourseSession, CourseSessionUser
from vbv_lernwelt.course.views import logger from vbv_lernwelt.course.views import logger
from vbv_lernwelt.course_session_group.models import CourseSessionGroup from vbv_lernwelt.course_session_group.models import CourseSessionGroup
from vbv_lernwelt.dashboard.graphql.types.dashboard import RoleKeyType, WidgetType
from vbv_lernwelt.learning_mentor.models import LearningMentor from vbv_lernwelt.learning_mentor.models import LearningMentor
@ -25,6 +26,20 @@ class CourseSessionWithRoles:
raise NotImplementedError("This proxy object cannot be saved.") raise NotImplementedError("This proxy object cannot be saved.")
@dataclass(frozen=True)
class CourseConfig:
course_id: int
course_slug: str
course_title: str
role_key: str
is_uk: bool
is_vv: bool
is_mentor: bool
widgets: List[str]
has_preview: bool
session_to_continue_id: str | None
def get_course_sessions_with_roles_for_user(user: User) -> List[CourseSessionWithRoles]: def get_course_sessions_with_roles_for_user(user: User) -> List[CourseSessionWithRoles]:
result_course_sessions = {} result_course_sessions = {}
@ -67,6 +82,18 @@ def get_course_sessions_with_roles_for_user(user: User) -> List[CourseSessionWit
] ]
def has_cs_role(roles: Set[str]) -> bool:
return bool(roles & {"SUPERVISOR", "EXPERT", "MEMBER"})
def user_role(roles: Set[str]) -> str:
return (
"SUPERVISOR"
if "SUPERVISOR" in roles
else ("EXPERT" if "EXPERT" in roles else "MEMBER")
)
@api_view(["GET"]) @api_view(["GET"])
def get_dashboard_persons(request): def get_dashboard_persons(request):
try: try:
@ -74,19 +101,11 @@ def get_dashboard_persons(request):
result_persons = {} result_persons = {}
for cs in course_sessions: for cs in course_sessions:
if { if has_cs_role(cs.roles) and cs.course.configuration.is_uk:
"SUPERVISOR",
"EXPERT",
"MEMBER",
} & cs.roles and cs.course.configuration.is_uk:
course_session_users = CourseSessionUser.objects.filter( course_session_users = CourseSessionUser.objects.filter(
course_session=cs.id course_session=cs.id
) )
my_role = ( my_role = user_role(cs.roles)
"SUPERVISOR"
if "SUPERVISOR" in cs.roles
else ("EXPERT" if "EXPERT" in cs.roles else "MEMBER")
)
for csu in course_session_users: for csu in course_session_users:
result_persons[csu.user.id] = { result_persons[csu.user.id] = {
"user_id": csu.user.id, "user_id": csu.user.id,
@ -183,3 +202,123 @@ def get_dashboard_persons(request):
except Exception as e: except Exception as e:
logger.error(e, exc_info=True) logger.error(e, exc_info=True)
return Response({"error": str(e)}, status=404) return Response({"error": str(e)}, status=404)
def get_widgets_for_course(
role_key: RoleKeyType, is_uk: bool, is_vv: bool, is_mentor: bool
) -> List[str]:
widgets = []
if role_key == RoleKeyType.MEMBER:
widgets.append(WidgetType.PROGRESS_WIDGET.value)
widgets.append(WidgetType.COMPETENCE_WIDGET.value)
if is_uk:
widgets.append(WidgetType.COMPETENCE_CERTIFICATE_WIDGET.value)
if is_mentor:
widgets.append(WidgetType.MENTOR_PERSON_WIDGET.value)
if is_vv:
widgets.append(WidgetType.MENTOR_TASKS_WIDGET.value)
if is_uk:
widgets.append(WidgetType.MENTOR_COMPETENCE_WIDGET.value)
return widgets
def get_role_and_mentor_key(
course_sessions: List[CourseSessionWithRoles], is_uk: bool, is_vv: bool
) -> tuple[RoleKeyType, bool]:
roles = set()
role = None
for cs in course_sessions:
roles.update(cs.roles)
if "SUPERVISOR" in roles:
role = RoleKeyType.SUPERVISOR
elif "EXPERT" in roles:
role = RoleKeyType.TRAINER
elif "MEMBER" in roles:
role = RoleKeyType.MEMBER
elif "LEARNING_MENTOR" in roles:
if is_uk:
role = RoleKeyType.MENTOR_UK
elif is_vv:
role = RoleKeyType.MENTOR_VV
is_mentor = "LEARNING_MENTOR" in roles
return role, is_mentor
def sort_course_sessions_by_course(
course_sessions: List[CourseSessionWithRoles],
) -> dict:
course_sessions_by_course = {}
for cs in course_sessions:
if cs.course.id not in course_sessions_by_course:
course_sessions_by_course[cs.course.id] = []
course_sessions_by_course[cs.course.id].append(cs)
return course_sessions_by_course
def has_preview(role_key: RoleKeyType) -> bool:
return role_key in [RoleKeyType.MENTOR_UK, RoleKeyType.MENTOR_VV]
def get_newest_cs(
course_sessions: List[CourseSessionWithRoles],
) -> CourseSessionWithRoles | None:
newest: CourseSessionWithRoles | None = None
for cs in course_sessions:
generation_newest = newest.generation if newest else None
if generation_newest is None or cs.generation > generation_newest:
newest = cs
return newest
def get_course_config(
course_sessions: List[CourseSessionWithRoles],
) -> List[CourseConfig]:
course_configs = []
cs_by_course = sort_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)
session_to_continue = get_newest_cs(cs_in_course)
course_configs.append(
CourseConfig(
course_id=cs_in_course[0].course.id,
course_slug=cs_in_course[0].course.slug,
course_title=cs_in_course[0].course.title,
role_key=role_key.value,
is_uk=is_uk,
is_vv=is_vv,
is_mentor=is_mentor,
widgets=get_widgets_for_course(role_key, is_uk, is_vv, is_mentor),
has_preview=has_preview(role_key),
session_to_continue_id=str(session_to_continue.id)
if session_to_continue
else None,
)
)
return course_configs
@api_view(["GET"])
def get_dashboard_config(request):
try:
course_sessions = get_course_sessions_with_roles_for_user(request.user) # noqa
course_configs = get_course_config(course_sessions)
return Response(
status=200,
data=[asdict(cc) for cc in course_configs],
)
except PermissionDenied as e:
raise e
except Exception as e:
logger.error(e, exc_info=True)
return Response({"error": str(e)}, status=404)