WIP: Add Praxisbildner switch, move code to type

This commit is contained in:
Christian Cueni 2024-04-03 11:14:44 +02:00
parent 6edb5be093
commit 2f77bf7734
8 changed files with 244 additions and 74 deletions

View File

@ -0,0 +1,41 @@
<script setup lang="ts">
import { computed, onMounted, ref, Ref } from "vue";
import { ProgressDashboardAssignmentType } from "@/gql/graphql";
import { fetchProgressData } from "@/services/dashboard";
import AssignmentProgressSummaryBox from "@/components/dashboard/AssignmentProgressSummaryBox.vue";
const props = defineProps<{
courseId: string;
courseSlug: string;
sessionToContinueId: string;
}>();
const DEFAULT_ASSIGNMENT = {
points_achieved_count: 0,
points_max_count: 0,
total_count: 0,
};
const assignment: Ref<ProgressDashboardAssignmentType> = ref(DEFAULT_ASSIGNMENT);
const competenceCertificateUrl = computed(() => {
return `/course/${props.courseSlug}/competence/certificates?courseSessionId=${props.sessionToContinueId}`;
});
onMounted(async () => {
const data = await fetchProgressData(props.courseId);
assignment.value = data?.assignment;
});
</script>
<template>
<div v-if="assignment" class="mb-14 space-y-8">
<div class="flex flex-col space-y-7 bg-white p-6">
<AssignmentProgressSummaryBox
:total-assignments="assignment.total_count"
:achieved-points-count="assignment.points_achieved_count"
:max-points-count="assignment.points_max_count"
:details-link="competenceCertificateUrl"
/>
</div>
</div>
</template>

View File

@ -18,8 +18,8 @@ const competenceCriteriaUrl = computed(() => {
}); });
onMounted(async () => { onMounted(async () => {
const some = await fetchProgressData(props.courseId); const data = await fetchProgressData(props.courseId);
competence.value = some.competence; competence.value = data.competence;
}); });
</script> </script>

View File

@ -1,10 +1,18 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, onMounted, ref } from "vue"; import { computed, onMounted, ref } from "vue";
import type { CourseProgressType } from "@/gql/graphql"; import type { CourseProgressType, WidgetType } from "@/gql/graphql";
import { DashboardConfigType } from "@/gql/graphql"; import { DashboardConfigType } from "@/gql/graphql";
import { fetchCourseData } from "@/services/dashboard"; 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";
const mentorWidgets = [
"MENTOR_TASKS_WIDGET",
"MENTOR_PERSON_WIDGET",
"MENTOR_COMPETENCE_WIDGET",
];
const progressWidgets = ["COMPETENCE_WIDGET", "COMPETENCE_CERTIFICATE_WIDGET"];
const props = defineProps<{ const props = defineProps<{
courseConfig: DashboardConfigType; courseConfig: DashboardConfigType;
@ -16,6 +24,17 @@ const data = ref<CourseProgressType | null>(null);
const courseSlug = computed(() => props.courseConfig?.slug); const courseSlug = computed(() => props.courseConfig?.slug);
const courseName = computed(() => props.courseConfig?.name); const courseName = computed(() => props.courseConfig?.name);
const numberOfMentorWidgets = computed(() => {
return data.value?.widgets.filter((widget) => mentorWidgets.includes(widget)).length;
});
const numberOfProgressWidgets = computed(() => {
return data.value?.widgets.filter((widget) => progressWidgets.includes(widget))
.length;
});
function hasWidget(widget: WidgetType) {
return data.value?.widgets.includes(widget);
}
onMounted(async () => { onMounted(async () => {
data.value = await fetchCourseData(props.courseConfig.id); data.value = await fetchCourseData(props.courseConfig.id);
@ -27,23 +46,31 @@ onMounted(async () => {
<div v-if="!isLoading && courseConfig" class="mb-14 space-y-8"> <div v-if="!isLoading && 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>
<div v-for="widget in data.widgets" :key="widget.id"> <LearningPathDiagram
{{ widget }} v-if="hasWidget('PROGRESS_WIDGET') && data.session_to_continue_id && courseSlug"
<LearningPathDiagram :key="courseSlug"
v-if=" :course-slug="courseSlug"
widget === 'PROGRESS_WIDGET' && data.session_to_continue_id && courseSlug :course-session-id="data.session_to_continue_id"
" diagram-type="horizontal"
:key="courseSlug" ></LearningPathDiagram>
:course-slug="courseSlug" <div v-if="numberOfProgressWidgets" class="flex flex-col flex-wrap">
:course-session-id="data.session_to_continue_id"
diagram-type="horizontal"
></LearningPathDiagram>
<CompetenceSummary <CompetenceSummary
v-else-if="widget === 'COMPETENCE_WIDGET'" v-if="hasWidget('COMPETENCE_WIDGET')"
:course-slug="courseSlug" :course-slug="courseSlug"
:session-to-continue-id="data.session_to_continue_id" :session-to-continue-id="data.session_to_continue_id"
:course-id="courseConfig.id" :course-id="courseConfig.id"
></CompetenceSummary> ></CompetenceSummary>
<AssignmentSummary
v-if="hasWidget('COMPETENCE_CERTIFICATE_WIDGET')"
:course-slug="courseSlug"
:session-to-continue-id="data.session_to_continue_id"
:course-id="courseConfig.id"
/>
</div>
<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('MENTOR_PERSON_WIDGET')">MENTOR_PERSON_WIDGET</div>
<div v-if="hasWidget('MENTOR_COMPETENCE_WIDGET')">MENTOR_COMPETENCE_WIDGET</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -466,6 +466,7 @@ export type DashboardConfigType = {
export type DashboardType = export type DashboardType =
| 'MENTOR_DASHBOARD' | 'MENTOR_DASHBOARD'
| 'PRAXISBILDNER_DASHBOARD'
| 'PROGRESS_DASHBOARD' | 'PROGRESS_DASHBOARD'
| 'SIMPLE_DASHBOARD' | 'SIMPLE_DASHBOARD'
| 'STATISTICS_DASHBOARD'; | 'STATISTICS_DASHBOARD';
@ -908,7 +909,6 @@ export type Query = {
course_session_attendance_course?: Maybe<CourseSessionAttendanceCourseObjectType>; course_session_attendance_course?: Maybe<CourseSessionAttendanceCourseObjectType>;
course_statistics?: Maybe<CourseStatisticsType>; course_statistics?: Maybe<CourseStatisticsType>;
dashboard_config: Array<DashboardConfigType>; dashboard_config: Array<DashboardConfigType>;
dashboard_data?: Maybe<DashboardConfigType>;
learning_content_assignment?: Maybe<LearningContentAssignmentObjectType>; learning_content_assignment?: Maybe<LearningContentAssignmentObjectType>;
learning_content_attendance_course?: Maybe<LearningContentAttendanceCourseObjectType>; learning_content_attendance_course?: Maybe<LearningContentAttendanceCourseObjectType>;
learning_content_document_list?: Maybe<LearningContentDocumentListObjectType>; learning_content_document_list?: Maybe<LearningContentDocumentListObjectType>;
@ -980,11 +980,6 @@ export type QueryCourseStatisticsArgs = {
}; };
export type QueryDashboardDataArgs = {
course_id: Scalars['ID']['input'];
};
export type QueryLearningPathArgs = { export type QueryLearningPathArgs = {
course_id?: InputMaybe<Scalars['ID']['input']>; course_id?: InputMaybe<Scalars['ID']['input']>;
course_slug?: InputMaybe<Scalars['String']['input']>; course_slug?: InputMaybe<Scalars['String']['input']>;
@ -1056,7 +1051,8 @@ export type UserObjectType = {
export type WidgetType = export type WidgetType =
| 'COMPETENCE_CERTIFICATE_WIDGET' | 'COMPETENCE_CERTIFICATE_WIDGET'
| 'COMPETENCE_WIDGET' | 'COMPETENCE_WIDGET'
| 'MENTEE_WIDGET' | 'MENTOR_COMPETENCE_WIDGET'
| 'MENTOR_PERSON_WIDGET'
| 'MENTOR_TASKS_WIDGET' | 'MENTOR_TASKS_WIDGET'
| 'PROGRESS_WIDGET'; | 'PROGRESS_WIDGET';

View File

@ -2,7 +2,6 @@ type Query {
course_statistics(course_id: ID!): CourseStatisticsType course_statistics(course_id: ID!): CourseStatisticsType
course_progress(course_id: ID!): CourseProgressType course_progress(course_id: ID!): CourseProgressType
dashboard_config: [DashboardConfigType!]! dashboard_config: [DashboardConfigType!]!
dashboard_data(course_id: ID!): DashboardConfigType
learning_path(id: ID, slug: String, course_id: ID, course_slug: String): LearningPathObjectType learning_path(id: ID, slug: String, course_id: ID, course_slug: String): LearningPathObjectType
course_session_attendance_course(id: ID!, assignment_user_id: ID): CourseSessionAttendanceCourseObjectType course_session_attendance_course(id: ID!, assignment_user_id: ID): CourseSessionAttendanceCourseObjectType
course(id: ID, slug: String): CourseObjectType course(id: ID, slug: String): CourseObjectType
@ -200,8 +199,9 @@ type ProgressDashboardAssignmentType {
enum WidgetType { enum WidgetType {
PROGRESS_WIDGET PROGRESS_WIDGET
COMPETENCE_WIDGET COMPETENCE_WIDGET
MENTEE_WIDGET
MENTOR_TASKS_WIDGET MENTOR_TASKS_WIDGET
MENTOR_PERSON_WIDGET
MENTOR_COMPETENCE_WIDGET
COMPETENCE_CERTIFICATE_WIDGET COMPETENCE_CERTIFICATE_WIDGET
} }
@ -218,6 +218,7 @@ enum DashboardType {
PROGRESS_DASHBOARD PROGRESS_DASHBOARD
SIMPLE_DASHBOARD SIMPLE_DASHBOARD
MENTOR_DASHBOARD MENTOR_DASHBOARD
PRAXISBILDNER_DASHBOARD
} }
type CourseConfigurationObjectType { type CourseConfigurationObjectType {

View File

@ -26,6 +26,7 @@ const boards: Record<DashboardType, DashboardPage> = {
SIMPLE_DASHBOARD: { main: SimpleCoursePage, aside: SimpleDates }, SIMPLE_DASHBOARD: { main: SimpleCoursePage, aside: SimpleDates },
STATISTICS_DASHBOARD: { main: StatisticPage, aside: CourseDetailDates }, STATISTICS_DASHBOARD: { main: StatisticPage, aside: CourseDetailDates },
MENTOR_DASHBOARD: { main: MentorPage, aside: SimpleDates }, MENTOR_DASHBOARD: { main: MentorPage, aside: SimpleDates },
PRAXISBILDNER_DASHBOARD: { main: CoursePanel, aside: SimpleDates },
}; };
onMounted(dashboardStore.loadDashboardDetails); onMounted(dashboardStore.loadDashboardDetails);
@ -57,8 +58,17 @@ onMounted(dashboardStore.loadDashboardDetails);
<CoursePanel :course-config="config" /> <CoursePanel :course-config="config" />
</li> </li>
</ul> </ul>
<!-- keep until we unify the dashboard -->
<CoursePanel
v-if="
dashboardStore.currentDashboardConfig.dashboard_type ===
'PRAXISBILDNER_DASHBOARD'
"
course-config="dashboardStore.currentDashboardConfig"
/>
<component <component
:is="boards[dashboardStore.currentDashboardConfig.dashboard_type].main" :is="boards[dashboardStore.currentDashboardConfig.dashboard_type].main"
v-else
></component> ></component>
</div> </div>
</main> </main>

View File

@ -15,9 +15,9 @@ from vbv_lernwelt.dashboard.graphql.types.dashboard import (
CourseStatisticsType, CourseStatisticsType,
DashboardConfigType, DashboardConfigType,
DashboardType, DashboardType,
get_widgets_for_course,
ProgressDashboardAssignmentType, ProgressDashboardAssignmentType,
ProgressDashboardCompetenceType, ProgressDashboardCompetenceType,
WidgetType,
) )
from vbv_lernwelt.iam.permissions import ( from vbv_lernwelt.iam.permissions import (
can_view_course_session, can_view_course_session,
@ -40,10 +40,6 @@ class DashboardQuery(graphene.ObjectType):
graphene.NonNull(DashboardConfigType), required=True graphene.NonNull(DashboardConfigType), required=True
) )
dashboard_data = graphene.Field(
DashboardConfigType, course_id=graphene.ID(required=True)
)
def resolve_course_statistics(root, info, course_id: str): # noqa def resolve_course_statistics(root, info, course_id: str): # noqa
user = info.context.user user = info.context.user
course = Course.objects.get(id=course_id) course = Course.objects.get(id=course_id)
@ -114,6 +110,8 @@ class DashboardQuery(graphene.ObjectType):
user = info.context.user user = info.context.user
course = Course.objects.get(id=course_id) course = Course.objects.get(id=course_id)
setattr(info.context, "course", course)
return CourseProgressType()
newest: CourseSession | None = None newest: CourseSession | None = None
course_session_for_user: List[str] = [] course_session_for_user: List[str] = []
@ -169,12 +167,9 @@ class DashboardQuery(graphene.ObjectType):
points_max_count=int(points_max_count), # noqa points_max_count=int(points_max_count), # noqa
points_achieved_count=int(points_achieved_count), # noqa points_achieved_count=int(points_achieved_count), # noqa
), ),
widgets=get_widget_for_course(course_id, user), # noqa widgets=get_widgets_for_course(course, user), # noqa
) )
def resolve_dashboard_data(root, info, course_id: str):
return get_widget_for_course(course_id, info.context.user)
def get_user_statistics_dashboards(user: User) -> Tuple[List[Dict[str, str]], Set[int]]: def get_user_statistics_dashboards(user: User) -> Tuple[List[Dict[str, str]], Set[int]]:
course_ids = set() course_ids = set()
@ -272,42 +267,3 @@ def get_user_course_session_dashboards(
) )
return dashboards, course_ids return dashboards, course_ids
def get_widget_for_course(course_id: str, user: User) -> List[WidgetType]:
widgets = []
course_sessions = CourseSession.objects.filter(course__id=course_id).prefetch_related(
"course",
"course__configuration",
)
roles_by_course: Dict[Course, Set[DashboardType]] = {}
learning_mentors = LearningMentor.objects.filter(mentor=user).values_list(
"course_session__course__id", "id"
)
mentor_course_ids = set([mentor[0] for mentor in learning_mentors])
# duplicate code
for course_session in course_sessions:
if can_view_course_session(user=user, course_session=course_session):
role = CourseSessionUser.objects.get(
course_session=course_session, user=user
).role
roles_by_course.setdefault(course_session.course, set())
roles_by_course[course_session.course].add(role)
for course, roles in roles_by_course.items():
if len(roles) == 1:
course_role = roles.pop()
# test widgets
if course_role == CourseSessionUser.Role.MEMBER:
widgets.append(WidgetType.PROGRESS_WIDGET)
widgets.append(WidgetType.COMPETENCE_WIDGET)
if course.configuration.enable_competence_certificates:
widgets.append(WidgetType.COMPETENCE_CERTIFICATE_WIDGET)
if course.id in mentor_course_ids:
widgets.append(WidgetType.MENTOR_TASKS_WIDGET)
# add KN if has KN
return widgets

View File

@ -1,8 +1,15 @@
from typing import Dict, List, Set, Tuple
import graphene import graphene
from graphene import Enum from graphene import Enum
from vbv_lernwelt.assignment.models import (
AssignmentCompletion,
AssignmentCompletionStatus,
)
from vbv_lernwelt.core.models import User
from vbv_lernwelt.course.graphql.types import CourseConfigurationObjectType from vbv_lernwelt.course.graphql.types import CourseConfigurationObjectType
from vbv_lernwelt.course.models import CourseSession, CourseSessionUser from vbv_lernwelt.course.models import Course, CourseSession, CourseSessionUser
from vbv_lernwelt.dashboard.graphql.types.assignment import ( from vbv_lernwelt.dashboard.graphql.types.assignment import (
assignments, assignments,
AssignmentsStatisticsType, AssignmentsStatisticsType,
@ -20,6 +27,11 @@ from vbv_lernwelt.dashboard.graphql.types.feedback import (
feedback_responses, feedback_responses,
FeedbackStatisticsResponsesType, FeedbackStatisticsResponsesType,
) )
from vbv_lernwelt.iam.permissions import (
can_view_course_session,
can_view_course_session_progress,
)
from vbv_lernwelt.learning_mentor.models import LearningMentor
from vbv_lernwelt.learnpath.models import Circle from vbv_lernwelt.learnpath.models import Circle
@ -54,13 +66,15 @@ class DashboardType(Enum):
PROGRESS_DASHBOARD = "ProgressDashboard" PROGRESS_DASHBOARD = "ProgressDashboard"
SIMPLE_DASHBOARD = "SimpleDashboard" SIMPLE_DASHBOARD = "SimpleDashboard"
MENTOR_DASHBOARD = "MentorDashboard" MENTOR_DASHBOARD = "MentorDashboard"
PRAXISBILDNER_DASHBOARD = "PraxisbildnerDashboard"
class WidgetType(Enum): class WidgetType(Enum):
PROGRESS_WIDGET = "ProgressWidget" PROGRESS_WIDGET = "ProgressWidget"
COMPETENCE_WIDGET = "CompetenceWidget" COMPETENCE_WIDGET = "CompetenceWidget"
MENTEE_WIDGET = "MenteeWidget"
MENTOR_TASKS_WIDGET = "MentorTasksWidget" MENTOR_TASKS_WIDGET = "MentorTasksWidget"
MENTOR_PERSON_WIDGET = "MentorPersonWidget"
MENTOR_COMPETENCE_WIDGET = "MentorCompetenceWidget"
COMPETENCE_CERTIFICATE_WIDGET = "CompetenceCertificateWidget" COMPETENCE_CERTIFICATE_WIDGET = "CompetenceCertificateWidget"
@ -99,6 +113,86 @@ class CourseProgressType(graphene.ObjectType):
assignment = graphene.Field(ProgressDashboardAssignmentType, required=False) assignment = graphene.Field(ProgressDashboardAssignmentType, required=False)
widgets = graphene.List(graphene.NonNull(WidgetType), required=True) widgets = graphene.List(graphene.NonNull(WidgetType), required=True)
def resolve__id(root, info):
return info.context.course.id
def resolve_course_id(root, info):
return info.context.course.id
def resolve_session_to_continue_id(root, info):
newest, _course_session_for_user = root._get_newest_cs_and_cs_for_user(
info, info.context.course.id, info.context.user
)
return newest.id if newest else None
def resolve_assignment(root, info):
evaluation_results = AssignmentCompletion.objects.filter(
completion_status=AssignmentCompletionStatus.EVALUATION_SUBMITTED.value,
assignment_user=info.context.user,
course_session__course=info.context.course,
).values("evaluation_max_points", "evaluation_points")
evaluation_results = list(evaluation_results)
points_max_count = sum(
[result.get("evaluation_max_points", 0) for result in evaluation_results]
)
points_achieved_count = sum(
[result.get("evaluation_points", 0) for result in evaluation_results]
)
return ProgressDashboardAssignmentType( # noqa
_id=info.context.course.id, # noqa
total_count=len(evaluation_results), # noqa
points_max_count=int(points_max_count), # noqa
points_achieved_count=int(points_achieved_count), # noqa
)
def resolve_competence(root, info):
newest, course_session_for_user = root._get_newest_cs_and_cs_for_user(
info, info.context.course.id, info.context.user
)
_, success_total, fail_total = competences(
course_slug=str(info.context.course.slug),
course_session_selection_ids=course_session_for_user,
user_selection_ids=[str(info.context.user.id)],
)
return ProgressDashboardCompetenceType( # noqa
_id=info.context.course.id, # noqa
total_count=success_total + fail_total, # noqa
success_count=success_total, # noqa
fail_count=fail_total, # noqa
)
def resolve_widgets(root, info):
return get_widgets_for_course(info.context.course, info.context.user)
def _get_newest_cs_and_cs_for_user(
root, info, course_id: str, user: User
) -> Tuple[CourseSession, List[str]]:
newest: CourseSession | None = getattr(info.context, "newest", None)
course_session_for_user: List[str] = getattr(
info.context, "course_session_for_user", []
)
if newest is not None and course_session_for_user:
return newest, course_session_for_user
for course_session in CourseSession.objects.filter(course_id=course_id):
if can_view_course_session_progress(
user=user, course_session=course_session
):
course_session_for_user.append(course_session)
generation_newest = newest.generation if newest else None
if (
generation_newest is None
or course_session.generation > generation_newest
):
newest = course_session
# cache for use in other resolvers
setattr(info.context, "newest", newest)
setattr(info.context, "course_session_for_user", course_session_for_user)
return newest, course_session_for_user
class CourseStatisticsType(graphene.ObjectType): class CourseStatisticsType(graphene.ObjectType):
_id = graphene.ID(required=True) _id = graphene.ID(required=True)
@ -224,3 +318,48 @@ class CourseStatisticsType(graphene.ObjectType):
generations=list(generations), # noqa generations=list(generations), # noqa
circles=circle_data, # noqa circles=circle_data, # noqa
) )
def get_widgets_for_course(course: Course, user: User) -> List[WidgetType]:
widgets = []
course_sessions = CourseSession.objects.filter(
course__id=course.id, coursesessionuser__user=user
).prefetch_related(
"course",
"course__configuration",
)
roles_by_course: Dict[Course, Set[DashboardType]] = {}
learning_mentors = LearningMentor.objects.filter(
mentor=user, course_session__course__id=course.id
).values_list("course_session__course__id", "id")
mentor_course_ids = set([mentor[0] for mentor in learning_mentors])
# duplicate code
for course_session in course_sessions:
if can_view_course_session(user=user, course_session=course_session):
role = CourseSessionUser.objects.get(
course_session=course_session, user=user
).role
roles_by_course.setdefault(course_session.course, set())
roles_by_course[course_session.course].add(role)
for course, roles in roles_by_course.items():
if len(roles) == 1:
course_role = roles.pop()
# members
if course_role == CourseSessionUser.Role.MEMBER:
widgets.append(WidgetType.PROGRESS_WIDGET)
widgets.append(WidgetType.COMPETENCE_WIDGET)
if course.configuration.enable_competence_certificates:
widgets.append(WidgetType.COMPETENCE_CERTIFICATE_WIDGET)
# mentors
if course.id in mentor_course_ids:
widgets.append(WidgetType.MENTOR_TASKS_WIDGET)
widgets.append(WidgetType.MENTOR_PERSON_WIDGET)
if course.configuration.enable_competence_certificates:
widgets.append(WidgetType.MENTOR_COMPETENCE_WIDGET)
return widgets