WIP: Add own components

This commit is contained in:
Christian Cueni 2024-04-02 13:03:29 +02:00
parent 24aa5d9b8c
commit 6edb5be093
13 changed files with 252 additions and 10 deletions

View File

@ -0,0 +1,36 @@
<script setup lang="ts">
import { computed, onMounted, ref, Ref } from "vue";
import type { ProgressDashboardCompetenceType } from "@/gql/graphql";
import CompetenceSummaryBox from "@/components/dashboard/CompetenceSummaryBox.vue";
import { fetchProgressData } from "@/services/dashboard";
const props = defineProps<{
courseId: string;
courseSlug: string;
sessionToContinueId: string;
}>();
const DEFAULT_COMPETENCE = { total_count: 0, success_count: 0, fail_count: 0 };
const competence: Ref<ProgressDashboardCompetenceType> = ref(DEFAULT_COMPETENCE);
const competenceCriteriaUrl = computed(() => {
return `/course/${props.courseSlug}/competence/self-evaluation-and-feedback?courseSessionId=${props.sessionToContinueId}`;
});
onMounted(async () => {
const some = await fetchProgressData(props.courseId);
competence.value = some.competence;
});
</script>
<template>
<div v-if="competence" class="mb-14 space-y-8">
<div class="flex flex-col space-y-7 bg-white p-6">
<CompetenceSummaryBox
:fail-count="competence.fail_count"
:success-count="competence.success_count"
:details-link="competenceCriteriaUrl"
/>
</div>
</div>
</template>

View File

@ -0,0 +1,50 @@
<script setup lang="ts">
import { computed, onMounted, ref } from "vue";
import type { CourseProgressType } from "@/gql/graphql";
import { DashboardConfigType } from "@/gql/graphql";
import { fetchCourseData } from "@/services/dashboard";
import LearningPathDiagram from "@/components/learningPath/LearningPathDiagram.vue";
import CompetenceSummary from "@/components/dashboard/CompetenceSummary.vue";
const props = defineProps<{
courseConfig: DashboardConfigType;
}>();
const isLoading = ref(true);
const data = ref<CourseProgressType | null>(null);
const courseSlug = computed(() => props.courseConfig?.slug);
const courseName = computed(() => props.courseConfig?.name);
onMounted(async () => {
data.value = await fetchCourseData(props.courseConfig.id);
isLoading.value = false;
});
</script>
<template>
<div v-if="!isLoading && courseConfig" class="mb-14 space-y-8">
<div class="flex flex-col space-y-7 bg-white p-6">
<h3>{{ courseName }}</h3>
<div v-for="widget in data.widgets" :key="widget.id">
{{ widget }}
<LearningPathDiagram
v-if="
widget === 'PROGRESS_WIDGET' && data.session_to_continue_id && courseSlug
"
:key="courseSlug"
:course-slug="courseSlug"
:course-session-id="data.session_to_continue_id"
diagram-type="horizontal"
></LearningPathDiagram>
<CompetenceSummary
v-else-if="widget === 'COMPETENCE_WIDGET'"
:course-slug="courseSlug"
:session-to-continue-id="data.session_to_continue_id"
:course-id="courseConfig.id"
></CompetenceSummary>
</div>
</div>
</div>
</template>

View File

@ -23,6 +23,7 @@ const documents = {
"\n query courseQuery($slug: String!) {\n course(slug: $slug) {\n id\n title\n slug\n category_name\n configuration {\n id\n enable_circle_documents\n enable_learning_mentor\n enable_competence_certificates\n }\n action_competences {\n competence_id\n ...CoursePageFields\n performance_criteria {\n competence_id\n learning_unit {\n id\n slug\n evaluate_url\n }\n ...CoursePageFields\n }\n }\n learning_path {\n ...CoursePageFields\n topics {\n is_visible\n ...CoursePageFields\n circles {\n description\n goals\n ...CoursePageFields\n learning_sequences {\n icon\n ...CoursePageFields\n learning_units {\n evaluate_url\n ...CoursePageFields\n performance_criteria {\n ...CoursePageFields\n }\n learning_contents {\n can_user_self_toggle_course_completion\n content_url\n minutes\n description\n ...CoursePageFields\n ... on LearningContentAssignmentObjectType {\n assignment_type\n content_assignment {\n id\n assignment_type\n }\n competence_certificate {\n ...CoursePageFields\n }\n }\n ... on LearningContentEdoniqTestObjectType {\n checkbox_text\n has_extended_time_test\n content_assignment {\n id\n assignment_type\n }\n competence_certificate {\n ...CoursePageFields\n }\n }\n ... on LearningContentRichTextObjectType {\n text\n }\n }\n }\n }\n }\n }\n }\n }\n }\n": types.CourseQueryDocument,
"\n query dashboardConfig {\n dashboard_config {\n id\n slug\n name\n dashboard_type\n course_configuration {\n id\n enable_circle_documents\n enable_learning_mentor\n enable_competence_certificates\n }\n }\n }\n": types.DashboardConfigDocument,
"\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 widgets\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 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,
};
@ -81,6 +82,10 @@ export function graphql(source: "\n query dashboardConfig {\n dashboard_conf
* 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 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"): (typeof 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"];
/**
* 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 dashboardCourseData($courseId: ID!) {\n course_progress(course_id: $courseId) {\n _id\n course_id\n session_to_continue_id\n widgets\n }\n }\n"): (typeof documents)["\n query dashboardCourseData($courseId: ID!) {\n course_progress(course_id: $courseId) {\n _id\n course_id\n session_to_continue_id\n widgets\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

@ -2,6 +2,7 @@ type Query {
course_statistics(course_id: ID!): CourseStatisticsType
course_progress(course_id: ID!): CourseProgressType
dashboard_config: [DashboardConfigType!]!
dashboard_data(course_id: ID!): DashboardConfigType
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(id: ID, slug: String): CourseObjectType
@ -177,8 +178,9 @@ type CourseProgressType {
_id: ID!
course_id: ID!
session_to_continue_id: ID
competence: ProgressDashboardCompetenceType!
assignment: ProgressDashboardAssignmentType!
competence: ProgressDashboardCompetenceType
assignment: ProgressDashboardAssignmentType
widgets: [WidgetType!]!
}
type ProgressDashboardCompetenceType {
@ -195,6 +197,14 @@ type ProgressDashboardAssignmentType {
points_achieved_count: Int!
}
enum WidgetType {
PROGRESS_WIDGET
COMPETENCE_WIDGET
MENTEE_WIDGET
MENTOR_TASKS_WIDGET
COMPETENCE_CERTIFICATE_WIDGET
}
type DashboardConfigType {
id: ID!
name: String!

View File

@ -84,3 +84,4 @@ export const String = "String";
export const TopicObjectType = "TopicObjectType";
export const UUID = "UUID";
export const UserObjectType = "UserObjectType";
export const WidgetType = "WidgetType";

View File

@ -331,6 +331,17 @@ export const DASHBOARD_COURSE_SESSION_PROGRESS = graphql(`
}
`);
export const DASHBOARD_COURSE_DATA = graphql(`
query dashboardCourseData($courseId: ID!) {
course_progress(course_id: $courseId) {
_id
course_id
session_to_continue_id
widgets
}
}
`);
export const DASHBOARD_COURSE_STATISTICS = graphql(`
query courseStatistics($courseId: ID!) {
course_statistics(course_id: $courseId) {

View File

@ -12,6 +12,7 @@ import LoadingSpinner from "@/components/ui/LoadingSpinner.vue";
import CourseDetailDates from "@/components/dashboard/CourseDetailDates.vue";
import NoCourseSession from "@/components/dashboard/NoCourseSession.vue";
import MentorPage from "@/pages/dashboard/MentorPage.vue";
import CoursePanel from "@/components/dashboard/CoursePanel.vue";
const dashboardStore = useDashboardStore();
@ -50,6 +51,12 @@ onMounted(dashboardStore.loadDashboardDetails);
></ItDropdownSelect>
</div>
<ul>
<li v-for="config in dashboardStore.dashboardConfigs" :key="config.id">
<p>{{ config }}</p>
<CoursePanel :course-config="config" />
</li>
</ul>
<component
:is="boards[dashboardStore.currentDashboardConfig.dashboard_type].main"
></component>

View File

@ -62,6 +62,7 @@ export async function handleCurrentCourseSession(to: RouteLocationNormalized) {
courseSessionsStore._currentCourseSlug = "";
}
if (!courseSessionsStore.loaded) {
console.log("handleCurrentCourseSession: loadCourseSessionsData");
await courseSessionsStore.loadCourseSessionsData();
}
}
@ -77,6 +78,7 @@ export async function handleCourseSessionAsQueryParam(to: RouteLocationNormalize
if (userStore.loggedIn) {
const courseSessionsStore = useCourseSessionsStore();
if (!courseSessionsStore.loaded) {
console.log("handleCourseSessionAsQueryParam: loadCourseSessionsData");
await courseSessionsStore.loadCourseSessionsData();
}

View File

@ -387,8 +387,25 @@ router.beforeEach(updateLoggedIn);
router.beforeEach(redirectToLoginIfRequired);
// register after login hooks
router.beforeEach(handleCurrentCourseSession);
router.beforeEach(handleCourseSessionAsQueryParam);
router.beforeEach(
async (to, from) =>
await ignoreGuardsForHomeRoute(to, from, handleCurrentCourseSession)
);
router.beforeEach(
async (to, from) =>
await ignoreGuardsForHomeRoute(to, from, handleCourseSessionAsQueryParam)
);
async function ignoreGuardsForHomeRoute(
to: RouteLocationNormalized,
from: RouteLocationNormalized,
guard: NavigationGuardNext
) {
if (to.name !== "home") {
return await guard(to, from);
}
}
router.beforeEach(addToHistory);

View File

@ -1,6 +1,7 @@
import { graphqlClient } from "@/graphql/client";
import {
DASHBOARD_CONFIG,
DASHBOARD_COURSE_DATA,
DASHBOARD_COURSE_SESSION_PROGRESS,
DASHBOARD_COURSE_STATISTICS,
} from "@/graphql/queries";
@ -61,3 +62,21 @@ export const fetchDashboardConfig = async (): Promise<DashboardConfigType[] | nu
return null;
}
};
export const fetchCourseData = async (
courseId: string
): Promise<CourseProgressType | null> => {
try {
const res = await graphqlClient.query(DASHBOARD_COURSE_DATA, {
courseId,
});
if (res.error) {
console.error("Error fetching data for course ID:", courseId, res.error);
}
return res.data?.course_progress || null;
} catch (error) {
console.error(`Error fetching data for course ID: ${courseId}`, error);
return null;
}
};

View File

@ -17,6 +17,7 @@ from vbv_lernwelt.dashboard.graphql.types.dashboard import (
DashboardType,
ProgressDashboardAssignmentType,
ProgressDashboardCompetenceType,
WidgetType,
)
from vbv_lernwelt.iam.permissions import (
can_view_course_session,
@ -39,6 +40,10 @@ class DashboardQuery(graphene.ObjectType):
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
user = info.context.user
course = Course.objects.get(id=course_id)
@ -164,8 +169,12 @@ class DashboardQuery(graphene.ObjectType):
points_max_count=int(points_max_count), # noqa
points_achieved_count=int(points_achieved_count), # noqa
),
widgets=get_widget_for_course(course_id, 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]]:
course_ids = set()
@ -222,7 +231,6 @@ def get_user_course_session_dashboards(
sessions of a course, but with varying permissions?
-> We just show the simple list dashboard for now.
"""
dashboards = []
course_ids = set()
@ -246,6 +254,7 @@ def get_user_course_session_dashboards(
resolved_dashboard_type = DashboardType.SIMPLE_DASHBOARD
elif course_role == CourseSessionUser.Role.MEMBER:
resolved_dashboard_type = DashboardType.PROGRESS_DASHBOARD
else:
# fallback: just go with simple list dashboard
resolved_dashboard_type = DashboardType.SIMPLE_DASHBOARD
@ -263,3 +272,42 @@ def get_user_course_session_dashboards(
)
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

@ -56,6 +56,14 @@ class DashboardType(Enum):
MENTOR_DASHBOARD = "MentorDashboard"
class WidgetType(Enum):
PROGRESS_WIDGET = "ProgressWidget"
COMPETENCE_WIDGET = "CompetenceWidget"
MENTEE_WIDGET = "MenteeWidget"
MENTOR_TASKS_WIDGET = "MentorTasksWidget"
COMPETENCE_CERTIFICATE_WIDGET = "CompetenceCertificateWidget"
class DashboardConfigType(graphene.ObjectType):
id = graphene.ID(required=True)
name = graphene.String(required=True)
@ -64,6 +72,11 @@ class DashboardConfigType(graphene.ObjectType):
course_configuration = graphene.Field(CourseConfigurationObjectType, required=True)
class DashboardDataType(graphene.ObjectType):
id = graphene.ID(required=True)
widgets = graphene.List(graphene.NonNull(WidgetType), required=True)
class ProgressDashboardCompetenceType(graphene.ObjectType):
_id = graphene.ID(required=True)
total_count = graphene.Int(required=True)
@ -82,8 +95,9 @@ class CourseProgressType(graphene.ObjectType):
_id = graphene.ID(required=True)
course_id = graphene.ID(required=True)
session_to_continue_id = graphene.ID(required=False)
competence = graphene.Field(ProgressDashboardCompetenceType, required=True)
assignment = graphene.Field(ProgressDashboardAssignmentType, required=True)
competence = graphene.Field(ProgressDashboardCompetenceType, required=False)
assignment = graphene.Field(ProgressDashboardAssignmentType, required=False)
widgets = graphene.List(graphene.NonNull(WidgetType), required=True)
class CourseStatisticsType(graphene.ObjectType):