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,
circleFlatLearningUnits,
} from "@/services/circle";
import type { DashboardPersonType } from "@/services/dashboard";
import { fetchDashboardPersons } from "@/services/dashboard";
import { presignUpload, uploadFile } from "@/services/files";
import { useCompletionStore } from "@/stores/completion";
import { useCourseSessionsStore } from "@/stores/courseSessions";
@ -487,3 +489,24 @@ export function useMyLearningMentors() {
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);
const headers = Object.assign(
@ -56,11 +60,11 @@ export const itPost = (url: RequestInfo, data: unknown, options: RequestInit = {
return response.json().catch(() => {
return Promise.resolve(null);
});
});
}) as Promise<T>;
};
export const itGet = (url: RequestInfo) => {
return itPost(url, {}, { method: "GET" });
export const itGet = <T>(url: RequestInfo) => {
return itPost<T>(url, {}, { method: "GET" });
};
export const itDelete = (url: RequestInfo) => {
@ -81,17 +85,17 @@ export function bustItGetCache(key?: string) {
}
}
export const itGetCached = (
export const itGetCached = <T>(
url: RequestInfo,
options = {
reload: false,
}
): Promise<any> => {
): Promise<T> => {
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({

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 {
path: to.path,
query: restOfQuery,
replace: true,
// replace: true,
};
} else {
// courseSessionId is invalid for current user -> redirect to home

View File

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

View File

@ -6,12 +6,40 @@ import {
DASHBOARD_COURSE_STATISTICS,
} from "@/graphql/queries";
import { itGetCached } from "@/fetchHelpers";
import type {
CourseProgressType,
CourseStatisticsType,
DashboardConfigType,
} 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 (
courseId: string
): Promise<CourseStatisticsType | null> => {
@ -48,6 +76,7 @@ export const fetchProgressData = async (
return null;
}
};
export const fetchDashboardConfig = async (): Promise<DashboardConfigType[] | null> => {
try {
const res = await graphqlClient.query(DASHBOARD_CONFIG, {});
@ -80,3 +109,7 @@ export const fetchCourseData = async (
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,
)
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 (
export_students,
export_students_and_trainers,
@ -115,6 +116,9 @@ urlpatterns = [
# notify
re_path(r"api/notify/email_notification_settings/$", email_notification_settings,
name='email_notification_settings'),
# dashboard
path(r"api/dashboard/persons/", get_dashboard_persons, name="get_dashboard_persons"),
# course
path(r"api/course/sessions/", get_course_sessions, name="get_course_sessions"),

View File

@ -26,7 +26,9 @@ from wagtail.blocks.list_block import ListBlock, ListValue
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 = (
CoursePage.objects.get(course_id=course_id)
.get_children()
@ -40,7 +42,6 @@ def create_uk_fahrzeug_casework(course_id=COURSE_UK, competence_certificate=None
needs_expert_evaluation=True,
competence_certificate=competence_certificate,
effort_required="ca. 5 Stunden",
solution_sample=ContentDocument.objects.get(title="Musterlösung Fahrzeug"),
intro_text=replace_whitespace(
"""
<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_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.append(
@ -3591,7 +3597,7 @@ def create_uk_reflection(course_id=COURSE_UK):
assignment = AssignmentFactory(
parent=assignment_list_page,
assignment_type=AssignmentType.REFLECTION.name,
title=f"Reflexion",
title="Reflexion",
effort_required="ca. 1 Stunde",
intro_text=replace_whitespace(
"""
@ -3747,7 +3753,7 @@ def create_uk_fr_reflection(course_id=COURSE_UK_FR, circle_title="Véhicule"):
assignment = AssignmentFactory(
parent=assignment_list_page,
assignment_type=AssignmentType.REFLECTION.name,
title=f"Reflexion",
title="Reflexion",
effort_required="",
intro_text=replace_whitespace(
"""
@ -3900,7 +3906,7 @@ def create_uk_it_reflection(course_id=COURSE_UK_FR, circle_title="Véhicule"):
assignment = AssignmentFactory(
parent=assignment_list_page,
assignment_type=AssignmentType.REFLECTION.name,
title=f"Riflessione",
title="Riflessione",
effort_required="",
intro_text=replace_whitespace(
"""
@ -4053,7 +4059,7 @@ def create_vv_reflection(
assignment = AssignmentFactory(
parent=assignment_list_page,
assignment_type=AssignmentType.REFLECTION.name,
title=f"Reflexion",
title="Reflexion",
effort_required="ca. 1 Stunde",
intro_text=replace_whitespace(
"""

View File

@ -23,6 +23,8 @@ class CourseConfigurationAdmin(admin.ModelAdmin):
"enable_circle_documents",
"enable_learning_mentor",
"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_default_collections()
create_default_content_documents()
if UserImage.objects.count() == 0 and ContentImage.objects.count() == 0:
create_default_images()
if with_documents:
create_default_content_documents()
if UserImage.objects.count() == 0 and ContentImage.objects.count() == 0:
create_default_images()
course: Course = create_test_course_with_categories()
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:
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_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:
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()
if with_sessions:
@ -187,7 +194,7 @@ def create_test_course(include_uk=True, include_vv=True, with_sessions=False):
csa = CourseSessionAssignment.objects.create(
course_session=cs_bern,
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))
@ -215,7 +222,7 @@ def create_test_course(include_uk=True, include_vv=True, with_sessions=False):
_csa = CourseSessionAssignment.objects.create(
course_session=cs_bern,
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
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)
lp = LearningPathFactory(title="Test Lernpfad", parent=course_page)
if include_uk:
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:
TopicFactory(title="Circle VV", is_visible=False, parent=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(
title=title,
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)}",
)
LearningContentAssignmentFactory(
title="Redlichkeitserklärung",
parent=circle,
content_assignment=Assignment.objects.get(
slug__startswith="test-lehrgang-assignment-redlichkeits"
(
LearningContentAssignmentFactory(
title="Redlichkeitserklärung",
parent=circle,
content_assignment=Assignment.objects.get(
slug__startswith="test-lehrgang-assignment-redlichkeits"
),
),
),
LearningContentAssignmentFactory(
title="Fahrzeug - Mein erstes Auto",
assignment_type="PREP_ASSIGNMENT",
parent=circle,
content_assignment=Assignment.objects.get(
slug__startswith="test-lehrgang-assignment-fahrzeug-mein-erstes-auto"
)
(
LearningContentAssignmentFactory(
title="Fahrzeug - Mein erstes Auto",
assignment_type="PREP_ASSIGNMENT",
parent=circle,
content_assignment=Assignment.objects.get(
slug__startswith="test-lehrgang-assignment-fahrzeug-mein-erstes-auto"
),
),
),
)
PerformanceCriteriaFactory(
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")
LearningUnitFactory(title="Transfer", parent=circle)
LearningContentAssignmentFactory(
title="Reflexion",
assignment_type="REFLECTION",
parent=circle,
content_assignment=Assignment.objects.get(
slug__startswith=f"test-lehrgang-assignment-reflexion"
(
LearningContentAssignmentFactory(
title="Reflexion",
assignment_type="REFLECTION",
parent=circle,
content_assignment=Assignment.objects.get(
slug__startswith="test-lehrgang-assignment-reflexion"
),
),
),
)
assignment = Assignment.objects.get(
slug__startswith="test-lehrgang-assignment-überprüfen-einer-motorfahrzeugs"
)
assignment.solution_sample = ContentDocument.objects.get(
title="Musterlösung Fahrzeug"
)
if with_documents:
assignment.solution_sample = ContentDocument.objects.get(
title="Musterlösung Fahrzeug"
)
assignment.save()
LearningContentAssignmentFactory(
title="Überprüfen einer Motorfahrzeug-Versicherungspolice",
@ -574,9 +590,9 @@ def create_test_circle_reisen(lp):
description="Willkommen im Lehrgang Versicherungsvermitler VBV",
)
LearningContentMediaLibraryFactory(
title=f"Mediathek Reisen",
title="Mediathek Reisen",
parent=circle,
content_url=f"/course/test-lehrgang/media/handlungsfelder/reisen",
content_url="/course/test-lehrgang/media/handlungsfelder/reisen",
)
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",
)
LearningContentAssignmentFactory(
title="Mein Kundenstamm",
assignment_type="PRAXIS_ASSIGNMENT",
parent=circle,
content_assignment=Assignment.objects.get(
slug__startswith="test-lehrgang-assignment-mein-kundenstamm"
(
LearningContentAssignmentFactory(
title="Mein Kundenstamm",
assignment_type="PRAXIS_ASSIGNMENT",
parent=circle,
content_assignment=Assignment.objects.get(
slug__startswith="test-lehrgang-assignment-mein-kundenstamm"
),
),
),
)
PerformanceCriteriaFactory(
parent=ActionCompetence.objects.get(competence_id="Y1"),
competence_id=f"Y1.1",
title=f"Ich bin fähig zu Reisen eine Gesprächsführung zu machen",
competence_id="Y1.1",
title="Ich bin fähig zu Reisen eine Gesprächsführung zu machen",
learning_unit=lu,
)
PerformanceCriteriaFactory(
parent=ActionCompetence.objects.get(competence_id="Y2"),
competence_id=f"Y2.1",
title=f"Ich bin fähig zu Reisen eine Analyse zu machen",
competence_id="Y2.1",
title="Ich bin fähig zu Reisen eine Analyse zu machen",
learning_unit=lu,
)
@ -627,7 +645,7 @@ def create_test_circle_reisen(lp):
parent=parent,
)
LearningContentPlaceholderFactory(
title=f"Fachcheck Reisen",
title="Fachcheck Reisen",
parent=parent,
)
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.
""".strip(),
body=RichText(
f"<h2>Lernmedien</h2>"
f"<h3>Allgemeines</h3>"
f"<ul><li>Mit Risiken im Strassenverkehr umgehen</li><li>Versicherungsschutz</li><li>Vertragsarten</li><li>Zusammenfassung</li></ul>"
"<h2>Lernmedien</h2>"
"<h3>Allgemeines</h3>"
"<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,
parent=media_lib_allgemeines,
body=RichText(
f"<h2>Lernmedien</h2>"
f"<h3>Allgemeines</h3>"
f"<ul><li>Mit Risiken im Strassenverkehr umgehen</li><li>Versicherungsschutz</li><li>Vertragsarten</li><li>Zusammenfassung</li></ul>"
"<h2>Lernmedien</h2>"
"<h3>Allgemeines</h3>"
"<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()
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:
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):
slug_changed = False
if not self.id is None:
if self.id is not None:
old_record = Page.objects.get(id=self.id).specific
if old_record.slug != self.slug:
self.set_url_path(self.get_parent())
@ -340,5 +340,8 @@ class CourseConfiguration(models.Model):
_("Kompetenzweise ein/aus"), default=True
)
is_vv = models.BooleanField(_("Versicherungsvermittler-Lehrgang"), default=False)
is_uk = models.BooleanField(_("ÜK-Lehrgang"), default=False)
def __str__(self):
return f"Course Configuration for '{self.course.title}'"

View File

@ -73,16 +73,22 @@ class CourseSessionSerializer(serializers.ModelSerializer):
course = serializers.SerializerMethodField()
due_dates = serializers.SerializerMethodField()
actions = serializers.SerializerMethodField()
user_roles = serializers.SerializerMethodField()
def get_course(self, obj):
return CourseSerializer(obj.course).data
def get_due_dates(self, obj):
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
def get_user_roles(self, obj):
if hasattr(obj, "roles"):
return list(obj.roles)
return []
class Meta:
model = CourseSession
fields = [
@ -95,6 +101,7 @@ class CourseSessionSerializer(serializers.ModelSerializer):
"end_date",
"due_dates",
"actions",
"user_roles",
]
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):
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
)
) or is_course_session_expert(user, course_session_id)
def has_media_library(user, course_session_id: int):
@ -66,7 +66,7 @@ def has_media_library(user, course_session_id: int):
).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)
if course_session is None:
@ -92,6 +92,15 @@ def is_learning_mentor_for_user(
).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):
if user.is_superuser:
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()
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:
if user.is_superuser:
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]:
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(
{
"learning-mentor": has_learning_mentor(user, course_session_id),
"learning-mentor::edit-mentors": can_edit_mentors(user, course_session_id),
"learning-mentor::guide-members": can_guide_members(
user, course_session_id
),
# roles
"is_supervisor": is_supervisor,
"is_expert": is_expert,
"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),
"media-library": has_media_library(user, course_session_id),
"appointments": has_appointments(user, course_session_id),
"expert-cockpit": is_course_session_expert(user, course_session_id),
"learning-path": is_course_session_member(user, course_session_id),
"competence-navi": is_course_session_member(user, course_session_id),
"complete-learning-content": can_complete_learning_content(
user, course_session_id
),
"media-library": is_supervisor or is_expert or is_member,
"appointments": is_supervisor or is_expert or is_member,
"expert-cockpit": is_expert,
"learning-path": is_member,
"competence-navi": is_member,
"complete-learning-content": is_expert or is_member,
}
)

View File

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