Merge branch 'feature/dashboard-persons' into feature/dashboard

This commit is contained in:
Christian Cueni 2024-04-08 08:12:27 +02:00
commit 5ba319e524
18 changed files with 657 additions and 129 deletions

View File

@ -7,6 +7,8 @@ import {
circleFlatLearningContents, circleFlatLearningContents,
circleFlatLearningUnits, circleFlatLearningUnits,
} from "@/services/circle"; } from "@/services/circle";
import type { DashboardPersonType } from "@/services/dashboard";
import { fetchDashboardPersons } from "@/services/dashboard";
import { presignUpload, uploadFile } from "@/services/files"; import { presignUpload, uploadFile } from "@/services/files";
import { useCompletionStore } from "@/stores/completion"; import { useCompletionStore } from "@/stores/completion";
import { useCourseSessionsStore } from "@/stores/courseSessions"; import { useCourseSessionsStore } from "@/stores/courseSessions";
@ -487,3 +489,24 @@ export function useMyLearningMentors() {
loading, loading,
}; };
} }
export function useDashboardPersons() {
const dashboardPersons = ref<DashboardPersonType[]>([]);
const loading = ref(false);
const fetchData = async () => {
loading.value = true;
try {
dashboardPersons.value = await fetchDashboardPersons();
} finally {
loading.value = false;
}
};
onMounted(fetchData);
return {
dashboardPersons,
loading,
};
}

View File

@ -20,7 +20,11 @@ export const itFetch = (url: RequestInfo, options: RequestInit) => {
}); });
}; };
export const itPost = (url: RequestInfo, data: unknown, options: RequestInit = {}) => { export const itPost = <T>(
url: RequestInfo,
data: unknown,
options: RequestInit = {}
) => {
options = Object.assign({}, options); options = Object.assign({}, options);
const headers = Object.assign( const headers = Object.assign(
@ -56,11 +60,11 @@ export const itPost = (url: RequestInfo, data: unknown, options: RequestInit = {
return response.json().catch(() => { return response.json().catch(() => {
return Promise.resolve(null); return Promise.resolve(null);
}); });
}); }) as Promise<T>;
}; };
export const itGet = (url: RequestInfo) => { export const itGet = <T>(url: RequestInfo) => {
return itPost(url, {}, { method: "GET" }); return itPost<T>(url, {}, { method: "GET" });
}; };
export const itDelete = (url: RequestInfo) => { export const itDelete = (url: RequestInfo) => {
@ -81,17 +85,17 @@ export function bustItGetCache(key?: string) {
} }
} }
export const itGetCached = ( export const itGetCached = <T>(
url: RequestInfo, url: RequestInfo,
options = { options = {
reload: false, reload: false,
} }
): Promise<any> => { ): Promise<T> => {
if (!itGetPromiseCache.has(url.toString()) || options.reload) { if (!itGetPromiseCache.has(url.toString()) || options.reload) {
itGetPromiseCache.set(url.toString(), itGet(url)); itGetPromiseCache.set(url.toString(), itGet<T>(url));
} }
return itGetPromiseCache.get(url.toString()) as Promise<any>; return itGetPromiseCache.get(url.toString()) as Promise<T>;
}; };
export const useCSRFFetch = createFetch({ export const useCSRFFetch = createFetch({

View File

@ -0,0 +1,97 @@
<script setup lang="ts">
import LoadingSpinner from "@/components/ui/LoadingSpinner.vue";
import log from "loglevel";
import { useDashboardPersons } from "@/composables";
log.debug("DashboardPersonsPage created");
const { loading: loadingPersons, dashboardPersons } = useDashboardPersons();
</script>
<template>
<div>
<div v-if="loadingPersons" class="m-8 flex justify-center">
<LoadingSpinner />
</div>
<div v-else class="bg-gray-200">
<div class="container-large">
<div class="bg-white px-4 py-2">
<div
v-for="person in dashboardPersons"
:key="person.user_id"
data-cy="person"
class="flex flex-col justify-between gap-4 border-b p-2 last:border-b-0 md:flex-row md:items-center md:justify-between md:gap-16"
>
<div class="w-full flex-auto md:w-1/3">
<div class="flex items-center space-x-2">
<img
:alt="person.last_name"
class="h-11 w-11 rounded-full"
:src="'/static/avatars/myvbv-default-avatar.png'"
/>
<div>
<div class="text-bold">
{{ person.first_name }}
{{ person.last_name }}
</div>
<div class="text-gray-900">{{ person.email }}</div>
</div>
</div>
</div>
<div class="w-full flex-auto items-start md:w-2/3">
<div
v-for="cs in person.course_sessions"
:key="cs.id"
class="w-full border-b pb-2 pt-2 first:pt-0 last:border-b-0 last:pb-0"
>
<div class="flex flex-col md:flex-row md:items-center">
<div class="md:w-1/2">
<div class="text-gray-900">{{ cs.course_title }}</div>
<div v-if="cs.is_uk">{{ cs.session_title }}</div>
</div>
<div class="md:w-1/4">
<div>{{ cs.user_role }}</div>
<div>my role: {{ cs.my_role }}</div>
</div>
<div class="md:w-1/4 md:text-right">
<div
v-if="
(['SUPERVISOR', 'EXPERT'].includes(cs.my_role) &&
cs.user_role === 'MEMBER') ||
(cs.my_role === 'LEARNING_MENTOR' &&
cs.user_role === 'LEARNING_MENTEE')
"
>
<router-link
:to="{
name: 'profileLearningPath',
params: {
userId: person.user_id,
courseSlug: cs.course_slug,
},
query: { courseSessionId: cs.id },
}"
class="link w-full lg:text-right"
>
{{ $t("a.Profil anzeigen") }}
</router-link>
</div>
</div>
</div>
</div>
</div>
<!-- <button-->
<!-- class="underline"-->
<!-- data-cy="lm-my-mentor-remove"-->
<!-- @click="removeMyMentor(learningMentor.id)"-->
<!-- >-->
<!-- {{ $t("a.Entfernen") }}-->
<!-- </button>-->
</div>
</div>
</div>
</div>
</div>
</template>

View File

@ -91,7 +91,7 @@ export async function handleCourseSessionAsQueryParam(to: RouteLocationNormalize
return { return {
path: to.path, path: to.path,
query: restOfQuery, query: restOfQuery,
replace: true, // replace: true,
}; };
} else { } else {
// courseSessionId is invalid for current user -> redirect to home // courseSessionId is invalid for current user -> redirect to home

View File

@ -60,6 +60,10 @@ const router = createRouter({
name: "home", name: "home",
component: DashboardPage, component: DashboardPage,
}, },
{
path: "/dashboard/persons",
component: () => import("@/pages/dashboard/DashboardPersonsPage.vue"),
},
{ {
path: "/course/:courseSlug/media", path: "/course/:courseSlug/media",
props: true, props: true,

View File

@ -6,12 +6,40 @@ import {
DASHBOARD_COURSE_STATISTICS, DASHBOARD_COURSE_STATISTICS,
} from "@/graphql/queries"; } from "@/graphql/queries";
import { itGetCached } from "@/fetchHelpers";
import type { import type {
CourseProgressType, CourseProgressType,
CourseStatisticsType, CourseStatisticsType,
DashboardConfigType, DashboardConfigType,
} from "@/gql/graphql"; } from "@/gql/graphql";
export type DashboardPersonRoleType =
| "SUPERVISOR"
| "EXPERT"
| "MEMBER"
| "LEARNING_MENTOR"
| "LEARNING_MENTEE";
export type DashboardPersonCourseSessionType = {
id: number;
session_title: string;
course_id: number;
course_title: string;
course_slug: string;
user_role: DashboardPersonRoleType;
my_role: DashboardPersonRoleType;
is_uk: boolean;
is_vv: boolean;
};
export type DashboardPersonType = {
user_id: string;
first_name: string;
last_name: string;
email: string;
course_sessions: DashboardPersonCourseSessionType[];
};
export const fetchStatisticData = async ( export const fetchStatisticData = async (
courseId: string courseId: string
): Promise<CourseStatisticsType | null> => { ): Promise<CourseStatisticsType | null> => {
@ -48,6 +76,7 @@ export const fetchProgressData = async (
return null; return null;
} }
}; };
export const fetchDashboardConfig = async (): Promise<DashboardConfigType[] | null> => { export const fetchDashboardConfig = async (): Promise<DashboardConfigType[] | null> => {
try { try {
const res = await graphqlClient.query(DASHBOARD_CONFIG, {}); const res = await graphqlClient.query(DASHBOARD_CONFIG, {});
@ -80,3 +109,7 @@ export const fetchCourseData = async (
return null; return null;
} }
}; };
export async function fetchDashboardPersons() {
return await itGetCached<DashboardPersonType[]>("/api/dashboard/persons/");
}

View File

@ -39,6 +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.edoniq_test.views import ( from vbv_lernwelt.edoniq_test.views import (
export_students, export_students,
export_students_and_trainers, export_students_and_trainers,
@ -116,6 +117,9 @@ urlpatterns = [
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
path(r"api/dashboard/persons/", get_dashboard_persons, name="get_dashboard_persons"),
# 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"),
path(r"api/course/page/<slug_or_id>/", course_page_api_view, path(r"api/course/page/<slug_or_id>/", course_page_api_view,

View File

@ -26,7 +26,9 @@ from wagtail.blocks.list_block import ListBlock, ListValue
from wagtail.rich_text import RichText from wagtail.rich_text import RichText
def create_uk_fahrzeug_casework(course_id=COURSE_UK, competence_certificate=None): def create_uk_fahrzeug_casework(
course_id=COURSE_UK, competence_certificate=None, with_documents=False
):
assignment_list_page = ( assignment_list_page = (
CoursePage.objects.get(course_id=course_id) CoursePage.objects.get(course_id=course_id)
.get_children() .get_children()
@ -40,7 +42,6 @@ def create_uk_fahrzeug_casework(course_id=COURSE_UK, competence_certificate=None
needs_expert_evaluation=True, needs_expert_evaluation=True,
competence_certificate=competence_certificate, competence_certificate=competence_certificate,
effort_required="ca. 5 Stunden", effort_required="ca. 5 Stunden",
solution_sample=ContentDocument.objects.get(title="Musterlösung Fahrzeug"),
intro_text=replace_whitespace( intro_text=replace_whitespace(
""" """
<h3>Ausgangslage</h3> <h3>Ausgangslage</h3>
@ -70,6 +71,11 @@ def create_uk_fahrzeug_casework(course_id=COURSE_UK, competence_certificate=None
evaluation_document_url="/static/media/assignments/UK_03_09_NACH_KN_Beurteilungsraster.pdf", evaluation_document_url="/static/media/assignments/UK_03_09_NACH_KN_Beurteilungsraster.pdf",
evaluation_description="Diese geleitete Fallarbeit wird auf Grund des folgenden Beurteilungsintrument bewertet.", evaluation_description="Diese geleitete Fallarbeit wird auf Grund des folgenden Beurteilungsintrument bewertet.",
) )
if with_documents:
assignment.solution_sample = ContentDocument.objects.get(
title="Musterlösung Fahrzeug"
)
assignment.save()
assignment.evaluation_tasks = [] assignment.evaluation_tasks = []
assignment.evaluation_tasks.append( assignment.evaluation_tasks.append(
@ -3591,7 +3597,7 @@ def create_uk_reflection(course_id=COURSE_UK):
assignment = AssignmentFactory( assignment = AssignmentFactory(
parent=assignment_list_page, parent=assignment_list_page,
assignment_type=AssignmentType.REFLECTION.name, assignment_type=AssignmentType.REFLECTION.name,
title=f"Reflexion", title="Reflexion",
effort_required="ca. 1 Stunde", effort_required="ca. 1 Stunde",
intro_text=replace_whitespace( intro_text=replace_whitespace(
""" """
@ -3747,7 +3753,7 @@ def create_uk_fr_reflection(course_id=COURSE_UK_FR, circle_title="Véhicule"):
assignment = AssignmentFactory( assignment = AssignmentFactory(
parent=assignment_list_page, parent=assignment_list_page,
assignment_type=AssignmentType.REFLECTION.name, assignment_type=AssignmentType.REFLECTION.name,
title=f"Reflexion", title="Reflexion",
effort_required="", effort_required="",
intro_text=replace_whitespace( intro_text=replace_whitespace(
""" """
@ -3900,7 +3906,7 @@ def create_uk_it_reflection(course_id=COURSE_UK_FR, circle_title="Véhicule"):
assignment = AssignmentFactory( assignment = AssignmentFactory(
parent=assignment_list_page, parent=assignment_list_page,
assignment_type=AssignmentType.REFLECTION.name, assignment_type=AssignmentType.REFLECTION.name,
title=f"Riflessione", title="Riflessione",
effort_required="", effort_required="",
intro_text=replace_whitespace( intro_text=replace_whitespace(
""" """
@ -4053,7 +4059,7 @@ def create_vv_reflection(
assignment = AssignmentFactory( assignment = AssignmentFactory(
parent=assignment_list_page, parent=assignment_list_page,
assignment_type=AssignmentType.REFLECTION.name, assignment_type=AssignmentType.REFLECTION.name,
title=f"Reflexion", title="Reflexion",
effort_required="ca. 1 Stunde", effort_required="ca. 1 Stunde",
intro_text=replace_whitespace( intro_text=replace_whitespace(
""" """

View File

@ -23,6 +23,8 @@ class CourseConfigurationAdmin(admin.ModelAdmin):
"enable_circle_documents", "enable_circle_documents",
"enable_learning_mentor", "enable_learning_mentor",
"enable_competence_certificates", "enable_competence_certificates",
"is_vv",
"is_uk",
] ]

View File

@ -97,12 +97,15 @@ from vbv_lernwelt.media_library.tests.media_library_factories import (
) )
def create_test_course(include_uk=True, include_vv=True, with_sessions=False): def create_test_course(
include_uk=True, include_vv=True, with_sessions=False, with_documents=False
):
# create_locales_for_wagtail() # create_locales_for_wagtail()
create_default_collections() create_default_collections()
create_default_content_documents() if with_documents:
if UserImage.objects.count() == 0 and ContentImage.objects.count() == 0: create_default_content_documents()
create_default_images() if UserImage.objects.count() == 0 and ContentImage.objects.count() == 0:
create_default_images()
course: Course = create_test_course_with_categories() course: Course = create_test_course_with_categories()
course.configuration.enable_learning_mentor = False course.configuration.enable_learning_mentor = False
@ -118,7 +121,9 @@ def create_test_course(include_uk=True, include_vv=True, with_sessions=False):
if include_uk: if include_uk:
create_uk_fahrzeug_casework( create_uk_fahrzeug_casework(
course_id=COURSE_TEST_ID, competence_certificate=competence_certificate course_id=COURSE_TEST_ID,
competence_certificate=competence_certificate,
with_documents=with_documents,
) )
create_uk_fahrzeug_prep_assignment(course_id=COURSE_TEST_ID) create_uk_fahrzeug_prep_assignment(course_id=COURSE_TEST_ID)
create_uk_condition_acceptance(course_id=COURSE_TEST_ID) create_uk_condition_acceptance(course_id=COURSE_TEST_ID)
@ -132,7 +137,9 @@ def create_test_course(include_uk=True, include_vv=True, with_sessions=False):
if include_vv: if include_vv:
create_vv_gewinnen_casework(course_id=COURSE_TEST_ID) create_vv_gewinnen_casework(course_id=COURSE_TEST_ID)
create_test_learning_path(include_uk=include_uk, include_vv=include_vv) create_test_learning_path(
include_uk=include_uk, include_vv=include_vv, with_documents=with_documents
)
create_test_media_library() create_test_media_library()
if with_sessions: if with_sessions:
@ -187,7 +194,7 @@ def create_test_course(include_uk=True, include_vv=True, with_sessions=False):
csa = CourseSessionAssignment.objects.create( csa = CourseSessionAssignment.objects.create(
course_session=cs_bern, course_session=cs_bern,
learning_content=LearningContentAssignment.objects.get( learning_content=LearningContentAssignment.objects.get(
slug=f"test-lehrgang-lp-circle-fahrzeug-lc-fahrzeug-mein-erstes-auto" slug="test-lehrgang-lp-circle-fahrzeug-lc-fahrzeug-mein-erstes-auto"
), ),
) )
next_monday = datetime.now() + relativedelta(weekday=MO(2)) next_monday = datetime.now() + relativedelta(weekday=MO(2))
@ -215,7 +222,7 @@ def create_test_course(include_uk=True, include_vv=True, with_sessions=False):
_csa = CourseSessionAssignment.objects.create( _csa = CourseSessionAssignment.objects.create(
course_session=cs_bern, course_session=cs_bern,
learning_content=LearningContentAssignment.objects.get( learning_content=LearningContentAssignment.objects.get(
slug=f"test-lehrgang-lp-circle-reisen-lc-mein-kundenstamm" slug="test-lehrgang-lp-circle-reisen-lc-mein-kundenstamm"
), ),
) )
@ -418,20 +425,22 @@ def create_test_course_with_categories(apps=None, schema_editor=None):
return course return course
def create_test_learning_path(include_uk=True, include_vv=True): def create_test_learning_path(include_uk=True, include_vv=True, with_documents=False):
course_page = CoursePage.objects.get(course_id=COURSE_TEST_ID) course_page = CoursePage.objects.get(course_id=COURSE_TEST_ID)
lp = LearningPathFactory(title="Test Lernpfad", parent=course_page) lp = LearningPathFactory(title="Test Lernpfad", parent=course_page)
if include_uk: if include_uk:
TopicFactory(title="Circle ÜK", is_visible=False, parent=lp) TopicFactory(title="Circle ÜK", is_visible=False, parent=lp)
create_test_uk_circle_fahrzeug(lp, title="Fahrzeug") create_test_uk_circle_fahrzeug(
lp, title="Fahrzeug", with_documents=with_documents
)
if include_vv: if include_vv:
TopicFactory(title="Circle VV", is_visible=False, parent=lp) TopicFactory(title="Circle VV", is_visible=False, parent=lp)
create_test_circle_reisen(lp) create_test_circle_reisen(lp)
def create_test_uk_circle_fahrzeug(lp, title="Fahrzeug"): def create_test_uk_circle_fahrzeug(lp, title="Fahrzeug", with_documents=False):
circle = CircleFactory( circle = CircleFactory(
title=title, title=title,
parent=lp, parent=lp,
@ -470,21 +479,25 @@ damit du erfolgreich mit deinem Lernpfad (durch-)starten kannst.
), ),
content_url=f"/course/{lp.get_course().slug}/media/handlungsfelder/{slugify(title)}", content_url=f"/course/{lp.get_course().slug}/media/handlungsfelder/{slugify(title)}",
) )
LearningContentAssignmentFactory( (
title="Redlichkeitserklärung", LearningContentAssignmentFactory(
parent=circle, title="Redlichkeitserklärung",
content_assignment=Assignment.objects.get( parent=circle,
slug__startswith="test-lehrgang-assignment-redlichkeits" content_assignment=Assignment.objects.get(
slug__startswith="test-lehrgang-assignment-redlichkeits"
),
), ),
), )
LearningContentAssignmentFactory( (
title="Fahrzeug - Mein erstes Auto", LearningContentAssignmentFactory(
assignment_type="PREP_ASSIGNMENT", title="Fahrzeug - Mein erstes Auto",
parent=circle, assignment_type="PREP_ASSIGNMENT",
content_assignment=Assignment.objects.get( parent=circle,
slug__startswith="test-lehrgang-assignment-fahrzeug-mein-erstes-auto" content_assignment=Assignment.objects.get(
slug__startswith="test-lehrgang-assignment-fahrzeug-mein-erstes-auto"
),
), ),
), )
PerformanceCriteriaFactory( PerformanceCriteriaFactory(
parent=ActionCompetence.objects.get(competence_id="X1"), parent=ActionCompetence.objects.get(competence_id="X1"),
@ -531,21 +544,24 @@ damit du erfolgreich mit deinem Lernpfad (durch-)starten kannst.
LearningSequenceFactory(title="Transfer", parent=circle, icon="it-icon-ls-end") LearningSequenceFactory(title="Transfer", parent=circle, icon="it-icon-ls-end")
LearningUnitFactory(title="Transfer", parent=circle) LearningUnitFactory(title="Transfer", parent=circle)
LearningContentAssignmentFactory( (
title="Reflexion", LearningContentAssignmentFactory(
assignment_type="REFLECTION", title="Reflexion",
parent=circle, assignment_type="REFLECTION",
content_assignment=Assignment.objects.get( parent=circle,
slug__startswith=f"test-lehrgang-assignment-reflexion" content_assignment=Assignment.objects.get(
slug__startswith="test-lehrgang-assignment-reflexion"
),
), ),
), )
assignment = Assignment.objects.get( assignment = Assignment.objects.get(
slug__startswith="test-lehrgang-assignment-überprüfen-einer-motorfahrzeugs" slug__startswith="test-lehrgang-assignment-überprüfen-einer-motorfahrzeugs"
) )
assignment.solution_sample = ContentDocument.objects.get( if with_documents:
title="Musterlösung Fahrzeug" assignment.solution_sample = ContentDocument.objects.get(
) title="Musterlösung Fahrzeug"
)
assignment.save() assignment.save()
LearningContentAssignmentFactory( LearningContentAssignmentFactory(
title="Überprüfen einer Motorfahrzeug-Versicherungspolice", title="Überprüfen einer Motorfahrzeug-Versicherungspolice",
@ -574,9 +590,9 @@ def create_test_circle_reisen(lp):
description="Willkommen im Lehrgang Versicherungsvermitler VBV", description="Willkommen im Lehrgang Versicherungsvermitler VBV",
) )
LearningContentMediaLibraryFactory( LearningContentMediaLibraryFactory(
title=f"Mediathek Reisen", title="Mediathek Reisen",
parent=circle, parent=circle,
content_url=f"/course/test-lehrgang/media/handlungsfelder/reisen", content_url="/course/test-lehrgang/media/handlungsfelder/reisen",
) )
LearningSequenceFactory(title="Analyse", parent=circle) LearningSequenceFactory(title="Analyse", parent=circle)
@ -596,25 +612,27 @@ def create_test_circle_reisen(lp):
content_url="https://s3.eu-central-1.amazonaws.com/myvbv-wbt.iterativ.ch/emma-und-ayla-campen-durch-amerika-analyse-xapi-FZoZOP9y/index.html", content_url="https://s3.eu-central-1.amazonaws.com/myvbv-wbt.iterativ.ch/emma-und-ayla-campen-durch-amerika-analyse-xapi-FZoZOP9y/index.html",
) )
LearningContentAssignmentFactory( (
title="Mein Kundenstamm", LearningContentAssignmentFactory(
assignment_type="PRAXIS_ASSIGNMENT", title="Mein Kundenstamm",
parent=circle, assignment_type="PRAXIS_ASSIGNMENT",
content_assignment=Assignment.objects.get( parent=circle,
slug__startswith="test-lehrgang-assignment-mein-kundenstamm" content_assignment=Assignment.objects.get(
slug__startswith="test-lehrgang-assignment-mein-kundenstamm"
),
), ),
), )
PerformanceCriteriaFactory( PerformanceCriteriaFactory(
parent=ActionCompetence.objects.get(competence_id="Y1"), parent=ActionCompetence.objects.get(competence_id="Y1"),
competence_id=f"Y1.1", competence_id="Y1.1",
title=f"Ich bin fähig zu Reisen eine Gesprächsführung zu machen", title="Ich bin fähig zu Reisen eine Gesprächsführung zu machen",
learning_unit=lu, learning_unit=lu,
) )
PerformanceCriteriaFactory( PerformanceCriteriaFactory(
parent=ActionCompetence.objects.get(competence_id="Y2"), parent=ActionCompetence.objects.get(competence_id="Y2"),
competence_id=f"Y2.1", competence_id="Y2.1",
title=f"Ich bin fähig zu Reisen eine Analyse zu machen", title="Ich bin fähig zu Reisen eine Analyse zu machen",
learning_unit=lu, learning_unit=lu,
) )
@ -627,7 +645,7 @@ def create_test_circle_reisen(lp):
parent=parent, parent=parent,
) )
LearningContentPlaceholderFactory( LearningContentPlaceholderFactory(
title=f"Fachcheck Reisen", title="Fachcheck Reisen",
parent=parent, parent=parent,
) )
LearningContentKnowledgeAssessmentFactory( LearningContentKnowledgeAssessmentFactory(
@ -757,9 +775,9 @@ def create_test_media_library():
die der Fahrzeugbesitzer und die Fahrzeugbesitzerin in einem grösseren Schadenfall oft nur schwer selbst aufbringen kann. die der Fahrzeugbesitzer und die Fahrzeugbesitzerin in einem grösseren Schadenfall oft nur schwer selbst aufbringen kann.
""".strip(), """.strip(),
body=RichText( body=RichText(
f"<h2>Lernmedien</h2>" "<h2>Lernmedien</h2>"
f"<h3>Allgemeines</h3>" "<h3>Allgemeines</h3>"
f"<ul><li>Mit Risiken im Strassenverkehr umgehen</li><li>Versicherungsschutz</li><li>Vertragsarten</li><li>Zusammenfassung</li></ul>" "<ul><li>Mit Risiken im Strassenverkehr umgehen</li><li>Versicherungsschutz</li><li>Vertragsarten</li><li>Zusammenfassung</li></ul>"
), ),
) )
@ -778,9 +796,9 @@ def create_test_media_library():
title=cat, title=cat,
parent=media_lib_allgemeines, parent=media_lib_allgemeines,
body=RichText( body=RichText(
f"<h2>Lernmedien</h2>" "<h2>Lernmedien</h2>"
f"<h3>Allgemeines</h3>" "<h3>Allgemeines</h3>"
f"<ul><li>Mit Risiken im Strassenverkehr umgehen</li><li>Versicherungsschutz</li><li>Vertragsarten</li><li>Zusammenfassung</li></ul>" "<ul><li>Mit Risiken im Strassenverkehr umgehen</li><li>Versicherungsschutz</li><li>Vertragsarten</li><li>Zusammenfassung</li></ul>"
), ),
) )

View File

@ -182,7 +182,7 @@ def command(course):
create_course_uk_it() create_course_uk_it()
if COURSE_TEST_ID in course: if COURSE_TEST_ID in course:
create_test_course(with_sessions=True) create_test_course(with_sessions=True, with_documents=True)
if COURSE_UK_TRAINING in course: if COURSE_UK_TRAINING in course:
create_course_training_de() create_course_training_de()

View File

@ -0,0 +1,67 @@
# Generated by Django 3.2.20 on 2024-04-03 09:32
from django.db import migrations, models
TEST_COURSE_ID = -1
UK_COURSE_IDS = [
-3, # uk-de
-6, # uk-training-de
-5, # uk-fr
-7, # uk-training-fr
-8, # uk-it
-9, # uk-training-it
]
VV_COURSE_IDS = [
-4, # vv-de
-10, # vv-fr
-11, # vv-it
-12, # vv-prüfung
]
def forward_migration(apps, schema_editor):
Course = apps.get_model("course", "Course")
CourseConfiguration = apps.get_model("course", "CourseConfiguration")
for course in Course.objects.all():
config, created = CourseConfiguration.objects.get_or_create(
course=course,
)
# -> disable unnecessary features
if course.id in UK_COURSE_IDS:
config.is_uk = True
elif course.id in VV_COURSE_IDS:
config.is_vv = True
config.save()
def backward_migration(apps, schema_editor):
pass
class Migration(migrations.Migration):
dependencies = [
("course", "0007_auto_20240226_1553"),
]
operations = [
migrations.AddField(
model_name="courseconfiguration",
name="is_uk",
field=models.BooleanField(default=False, verbose_name="ÜK-Lehrgang"),
),
migrations.AddField(
model_name="courseconfiguration",
name="is_vv",
field=models.BooleanField(
default=False, verbose_name="Versicherungsvermittler-Lehrgang"
),
),
migrations.RunPython(forward_migration, backward_migration),
]

View File

@ -149,7 +149,7 @@ class CourseBasePage(Page):
def save(self, clean=True, user=None, log_action=False, **kwargs): def save(self, clean=True, user=None, log_action=False, **kwargs):
slug_changed = False slug_changed = False
if not self.id is None: if self.id is not None:
old_record = Page.objects.get(id=self.id).specific old_record = Page.objects.get(id=self.id).specific
if old_record.slug != self.slug: if old_record.slug != self.slug:
self.set_url_path(self.get_parent()) self.set_url_path(self.get_parent())
@ -340,5 +340,8 @@ class CourseConfiguration(models.Model):
_("Kompetenzweise ein/aus"), default=True _("Kompetenzweise ein/aus"), default=True
) )
is_vv = models.BooleanField(_("Versicherungsvermittler-Lehrgang"), default=False)
is_uk = models.BooleanField(_("ÜK-Lehrgang"), default=False)
def __str__(self): def __str__(self):
return f"Course Configuration for '{self.course.title}'" return f"Course Configuration for '{self.course.title}'"

View File

@ -73,16 +73,22 @@ class CourseSessionSerializer(serializers.ModelSerializer):
course = serializers.SerializerMethodField() course = serializers.SerializerMethodField()
due_dates = serializers.SerializerMethodField() due_dates = serializers.SerializerMethodField()
actions = serializers.SerializerMethodField() actions = serializers.SerializerMethodField()
user_roles = serializers.SerializerMethodField()
def get_course(self, obj): def get_course(self, obj):
return CourseSerializer(obj.course).data return CourseSerializer(obj.course).data
def get_due_dates(self, obj): def get_due_dates(self, obj):
due_dates = DueDate.objects.filter( due_dates = DueDate.objects.filter(
Q(start__isnull=False) | Q(end__isnull=False), course_session=obj Q(start__isnull=False) | Q(end__isnull=False), course_session_id=obj.id
) )
return DueDateSerializer(due_dates, many=True).data return DueDateSerializer(due_dates, many=True).data
def get_user_roles(self, obj):
if hasattr(obj, "roles"):
return list(obj.roles)
return []
class Meta: class Meta:
model = CourseSession model = CourseSession
fields = [ fields = [
@ -95,6 +101,7 @@ class CourseSessionSerializer(serializers.ModelSerializer):
"end_date", "end_date",
"due_dates", "due_dates",
"actions", "actions",
"user_roles",
] ]
read_only_fields = ["actions"] read_only_fields = ["actions"]

View File

@ -0,0 +1,86 @@
from django.test import TestCase
from vbv_lernwelt.course.creators.test_utils import (
add_course_session_group_supervisor,
add_course_session_user,
create_course,
create_course_session,
create_course_session_group,
create_user,
)
from vbv_lernwelt.course.models import CourseSessionUser
from vbv_lernwelt.dashboard.views import get_course_sessions_with_roles_for_user
from vbv_lernwelt.learning_mentor.models import LearningMentor
class GetCourseSessionsForUserTestCase(TestCase):
def setUp(self):
self.course, _ = create_course("Test Course")
self.course_session = create_course_session(
course=self.course, title="Test Session"
)
def test_participant_get_sessions(self):
# participant gets all his sessions marked with role "MEMBER"
participant = create_user("participant")
add_course_session_user(
self.course_session,
participant,
role=CourseSessionUser.Role.MEMBER,
)
# WHEN
sessions = get_course_sessions_with_roles_for_user(participant)
# THEN
self.assertEqual(len(sessions), 1)
self.assertEqual(sessions[0].title, "Test Session")
self.assertSetEqual(sessions[0].roles, {"MEMBER"})
def test_trainer_get_sessions(self):
# GIVEN
# trainer gets all his sessions marked with role "EXPERT"
trainer = create_user("trainer")
add_course_session_user(
self.course_session,
trainer,
role=CourseSessionUser.Role.EXPERT,
)
# WHEN
sessions = get_course_sessions_with_roles_for_user(trainer)
# THEN
self.assertEqual(len(sessions), 1)
self.assertEqual(sessions[0].title, "Test Session")
self.assertSetEqual(sessions[0].roles, {"EXPERT"})
def test_supervisor_get_sessions(self):
supervisor = create_user("supervisor")
group = create_course_session_group(course_session=self.course_session)
add_course_session_group_supervisor(group=group, user=supervisor)
sessions = get_course_sessions_with_roles_for_user(supervisor)
# THEN
self.assertEqual(len(sessions), 1)
self.assertEqual(sessions[0].title, "Test Session")
self.assertEqual(sessions[0].roles, {"SUPERVISOR"})
def test_learning_mentor_get_sessions(self):
mentor = create_user("mentor")
LearningMentor.objects.create(mentor=mentor, course_session=self.course_session)
participant = create_user("participant")
add_course_session_user(
self.course_session,
participant,
role=CourseSessionUser.Role.MEMBER,
)
sessions = get_course_sessions_with_roles_for_user(mentor)
# THEN
self.assertEqual(len(sessions), 1)
self.assertEqual(sessions[0].title, "Test Session")
self.assertEqual(sessions[0].roles, {"LEARNING_MENTOR"})

View File

@ -0,0 +1,185 @@
from dataclasses import dataclass
from typing import List, Set
from rest_framework.decorators import api_view
from rest_framework.exceptions import PermissionDenied
from rest_framework.response import Response
from vbv_lernwelt.core.models import User
from vbv_lernwelt.course.models import CourseSession, CourseSessionUser
from vbv_lernwelt.course.views import logger
from vbv_lernwelt.course_session_group.models import CourseSessionGroup
from vbv_lernwelt.learning_mentor.models import LearningMentor
@dataclass(frozen=True)
class CourseSessionWithRoles:
_original: CourseSession
roles: Set[str]
def __getattr__(self, name: str):
# Delegate attribute access to the _original CourseSession object
return getattr(self._original, name)
def save(self, *args, **kwargs):
raise NotImplementedError("This proxy object cannot be saved.")
def get_course_sessions_with_roles_for_user(user: User) -> List[CourseSessionWithRoles]:
result_course_sessions = {}
# participant/member/expert course sessions
csu_qs = CourseSessionUser.objects.filter(user=user).prefetch_related(
"course_session", "course_session__course"
)
for csu in csu_qs:
cs = csu.course_session
# member/expert is mutually exclusive...
cs.roles = {csu.role}
result_course_sessions[cs.id] = cs
# enrich with supervisor course sessions
csg_qs = CourseSessionGroup.objects.filter(supervisor=user).prefetch_related(
"course_session", "course_session__course"
)
for csg in csg_qs:
for cs in csg.course_session.all():
cs.roles = set()
cs = result_course_sessions.get(cs.id, cs)
cs.roles.add("SUPERVISOR")
result_course_sessions[cs.id] = cs
# enrich with mentor course sessions
lm_qs = LearningMentor.objects.filter(mentor=user).prefetch_related(
"course_session", "course_session__course"
)
for lm in lm_qs:
cs = lm.course_session
cs.roles = set()
cs = result_course_sessions.get(cs.id, cs)
cs.roles.add("LEARNING_MENTOR")
result_course_sessions[cs.id] = cs
return [
CourseSessionWithRoles(cs, cs.roles) for cs in result_course_sessions.values()
]
@api_view(["GET"])
def get_dashboard_persons(request):
try:
course_sessions = get_course_sessions_with_roles_for_user(request.user)
result_persons = {}
for cs in course_sessions:
if {
"SUPERVISOR",
"EXPERT",
"MEMBER",
} & cs.roles and cs.course.configuration.is_uk:
course_session_users = CourseSessionUser.objects.filter(
course_session=cs.id
)
my_role = (
"SUPERVISOR"
if "SUPERVISOR" in cs.roles
else ("EXPERT" if "EXPERT" in cs.roles else "MEMBER")
)
for csu in course_session_users:
result_persons[csu.user.id] = {
"user_id": csu.user.id,
"first_name": csu.user.first_name,
"last_name": csu.user.last_name,
"email": csu.user.email,
"course_sessions": [
{
"id": cs.id,
"session_title": cs.title,
"course_id": cs.course.id,
"course_title": cs.course.title,
"course_slug": cs.course.slug,
"user_role": csu.role,
"my_role": my_role,
"is_uk": cs.course.configuration.is_uk,
"is_vv": cs.course.configuration.is_vv,
}
],
}
# add persons where request.user is mentor
for cs in course_sessions:
if "LEARNING_MENTOR" in cs.roles:
lm = LearningMentor.objects.filter(
mentor=request.user, course_session=cs.id
).first()
for participant in lm.participants.all():
course_session_entry = {
"id": cs.id,
"session_title": cs.title,
"course_id": cs.course.id,
"course_title": cs.course.title,
"course_slug": cs.course.slug,
"user_role": "LEARNING_MENTEE",
"my_role": "LEARNING_MENTOR",
"is_uk": cs.course.configuration.is_uk,
"is_vv": cs.course.configuration.is_vv,
}
if participant.user.id not in result_persons:
result_persons[participant.user.id] = {
"user_id": participant.user.id,
"first_name": participant.user.first_name,
"last_name": participant.user.last_name,
"email": participant.user.email,
"course_sessions": [course_session_entry],
}
else:
# user is already in result_persons
result_persons[participant.user.id]["course_sessions"].append(
course_session_entry
)
# add persons where request.user is mentee
mentor_relation_qs = LearningMentor.objects.filter(
participants__user=request.user
).prefetch_related("mentor", "course_session")
for mentor_relation in mentor_relation_qs:
cs = mentor_relation.course_session
course_session_entry = {
"id": cs.id,
"session_title": cs.title,
"course_id": cs.course.id,
"course_title": cs.course.title,
"course_slug": cs.course.slug,
"user_role": "LEARNING_MENTOR",
"my_role": "LEARNING_MENTEE",
"is_uk": cs.course.configuration.is_uk,
"is_vv": cs.course.configuration.is_vv,
}
if mentor_relation.mentor.id not in result_persons:
result_persons[mentor_relation.mentor.id] = {
"user_id": mentor_relation.mentor.id,
"first_name": mentor_relation.mentor.first_name,
"last_name": mentor_relation.mentor.last_name,
"email": mentor_relation.mentor.email,
"course_sessions": [course_session_entry],
}
else:
# user is already in result_persons
result_persons[mentor_relation.mentor.id]["course_sessions"].append(
course_session_entry
)
return Response(
status=200,
data=list(result_persons.values()),
)
except PermissionDenied as e:
raise e
except Exception as e:
logger.error(e, exc_info=True)
return Response({"error": str(e)}, status=404)

View File

@ -48,9 +48,9 @@ def has_course_session_preview(user, course_session_id: int):
if is_course_session_member(user, course_session_id): if is_course_session_member(user, course_session_id):
return False return False
return is_learning_mentor(user, course_session_id) or is_course_session_expert( return is_course_session_learning_mentor(
user, course_session_id user, course_session_id
) ) or is_course_session_expert(user, course_session_id)
def has_media_library(user, course_session_id: int): def has_media_library(user, course_session_id: int):
@ -66,7 +66,7 @@ def has_media_library(user, course_session_id: int):
).exists() ).exists()
def is_learning_mentor(mentor: User, course_session_id: int): def is_course_session_learning_mentor(mentor: User, course_session_id: int):
course_session = CourseSession.objects.get(id=course_session_id) course_session = CourseSession.objects.get(id=course_session_id)
if course_session is None: if course_session is None:
@ -92,6 +92,15 @@ def is_learning_mentor_for_user(
).exists() ).exists()
def is_course_session_supervisor(user, course_session_id: int):
if user.is_superuser:
return True
return CourseSessionGroup.objects.filter(
supervisor=user, course_session=course_session_id
).exists()
def is_course_session_expert(user, course_session_id: int): def is_course_session_expert(user, course_session_id: int):
if user.is_superuser: if user.is_superuser:
return True return True
@ -244,40 +253,6 @@ def has_appointments(user: User, course_session_id: int) -> bool:
return CourseSessionUser.objects.filter(course_session=course_session_id).exists() return CourseSessionUser.objects.filter(course_session=course_session_id).exists()
def has_learning_mentor(user: User, course_session_id: int) -> bool:
course_session = CourseSession.objects.get(id=course_session_id)
if course_session is None:
return False
if not course_session.course.configuration.enable_learning_mentor:
return False
if is_learning_mentor(user, course_session_id):
return True
if is_course_session_member(user, course_session_id):
return True
return False
def can_edit_mentors(user: User, course_session_id: int) -> bool:
if not has_learning_mentor(user, course_session_id):
return False
# limit further, since has_learning_mentor is too broad
return is_course_session_member(user, course_session_id)
def can_guide_members(user: User, course_session_id: int) -> bool:
if not has_learning_mentor(user, course_session_id):
return False
# limit further, since has_learning_mentor is too broad
return is_learning_mentor(user, course_session_id)
def can_view_profile(user: User, profile_user: CourseSessionUser) -> bool: def can_view_profile(user: User, profile_user: CourseSessionUser) -> bool:
if user.is_superuser: if user.is_superuser:
return True return True
@ -311,29 +286,40 @@ def can_view_course_completions(
) )
def can_complete_learning_content(user: User, course_session_id: int) -> bool:
return is_course_session_member(
user, course_session_id
) or is_course_session_expert(user, course_session_id)
def course_session_permissions(user: User, course_session_id: int) -> list[str]: def course_session_permissions(user: User, course_session_id: int) -> list[str]:
course_session = CourseSession.objects.get(id=course_session_id)
is_supervisor = is_course_session_supervisor(user, course_session_id)
is_expert = is_course_session_expert(user, course_session_id)
is_member = is_course_session_member(user, course_session_id)
is_learning_mentor = is_course_session_learning_mentor(user, course_session_id)
course_has_learning_mentor = (
course_session.course.configuration.enable_learning_mentor
)
has_learning_mentor = course_has_learning_mentor and (
is_member or is_expert or is_learning_mentor
)
return _action_list( return _action_list(
{ {
"learning-mentor": has_learning_mentor(user, course_session_id), # roles
"learning-mentor::edit-mentors": can_edit_mentors(user, course_session_id), "is_supervisor": is_supervisor,
"learning-mentor::guide-members": can_guide_members( "is_expert": is_expert,
user, course_session_id "is_member": is_member,
), "is_learning_mentor": is_learning_mentor,
# actions
"learning-mentor": has_learning_mentor,
"learning-mentor::edit-mentors": has_learning_mentor and is_member,
"learning-mentor::guide-members": course_has_learning_mentor
and is_learning_mentor,
"preview": has_course_session_preview(user, course_session_id), "preview": has_course_session_preview(user, course_session_id),
"media-library": has_media_library(user, course_session_id), "media-library": is_supervisor or is_expert or is_member,
"appointments": has_appointments(user, course_session_id), "appointments": is_supervisor or is_expert or is_member,
"expert-cockpit": is_course_session_expert(user, course_session_id), "expert-cockpit": is_expert,
"learning-path": is_course_session_member(user, course_session_id), "learning-path": is_member,
"competence-navi": is_course_session_member(user, course_session_id), "competence-navi": is_member,
"complete-learning-content": can_complete_learning_content( "complete-learning-content": is_expert or is_member,
user, course_session_id
),
} }
) )

View File

@ -48,15 +48,16 @@ class ActionTestCase(TestCase):
self.assertEqual( self.assertEqual(
mentor_actions, mentor_actions,
[ [
"is_learning_mentor",
"learning-mentor", "learning-mentor",
"learning-mentor::guide-members", "learning-mentor::guide-members",
"preview", "preview",
"appointments",
], ],
) )
self.assertEqual( self.assertEqual(
participant_actions, participant_actions,
[ [
"is_member",
"learning-mentor", "learning-mentor",
"learning-mentor::edit-mentors", "learning-mentor::edit-mentors",
"media-library", "media-library",
@ -69,6 +70,8 @@ class ActionTestCase(TestCase):
self.assertEqual( self.assertEqual(
trainer_actions, trainer_actions,
[ [
"is_expert",
"learning-mentor",
"preview", "preview",
"media-library", "media-library",
"appointments", "appointments",