diff --git a/client/src/composables.ts b/client/src/composables.ts index a4989aa0..d778f90c 100644 --- a/client/src/composables.ts +++ b/client/src/composables.ts @@ -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([]); + const loading = ref(false); + + const fetchData = async () => { + loading.value = true; + try { + dashboardPersons.value = await fetchDashboardPersons(); + } finally { + loading.value = false; + } + }; + + onMounted(fetchData); + + return { + dashboardPersons, + loading, + }; +} diff --git a/client/src/fetchHelpers.ts b/client/src/fetchHelpers.ts index f4ca9ec1..bbdd50db 100644 --- a/client/src/fetchHelpers.ts +++ b/client/src/fetchHelpers.ts @@ -20,7 +20,11 @@ export const itFetch = (url: RequestInfo, options: RequestInit) => { }); }; -export const itPost = (url: RequestInfo, data: unknown, options: RequestInit = {}) => { +export const itPost = ( + 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; }; -export const itGet = (url: RequestInfo) => { - return itPost(url, {}, { method: "GET" }); +export const itGet = (url: RequestInfo) => { + return itPost(url, {}, { method: "GET" }); }; export const itDelete = (url: RequestInfo) => { @@ -81,17 +85,17 @@ export function bustItGetCache(key?: string) { } } -export const itGetCached = ( +export const itGetCached = ( url: RequestInfo, options = { reload: false, } -): Promise => { +): Promise => { if (!itGetPromiseCache.has(url.toString()) || options.reload) { - itGetPromiseCache.set(url.toString(), itGet(url)); + itGetPromiseCache.set(url.toString(), itGet(url)); } - return itGetPromiseCache.get(url.toString()) as Promise; + return itGetPromiseCache.get(url.toString()) as Promise; }; export const useCSRFFetch = createFetch({ diff --git a/client/src/pages/dashboard/DashboardPersonsPage.vue b/client/src/pages/dashboard/DashboardPersonsPage.vue new file mode 100644 index 00000000..b4a96cc3 --- /dev/null +++ b/client/src/pages/dashboard/DashboardPersonsPage.vue @@ -0,0 +1,97 @@ + + + diff --git a/client/src/router/guards.ts b/client/src/router/guards.ts index 8ee4825a..7e8b1ed6 100644 --- a/client/src/router/guards.ts +++ b/client/src/router/guards.ts @@ -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 diff --git a/client/src/router/index.ts b/client/src/router/index.ts index 14c9eb43..ae99241c 100644 --- a/client/src/router/index.ts +++ b/client/src/router/index.ts @@ -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, diff --git a/client/src/services/dashboard.ts b/client/src/services/dashboard.ts index b4232d64..2beaf0a3 100644 --- a/client/src/services/dashboard.ts +++ b/client/src/services/dashboard.ts @@ -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 => { @@ -48,6 +76,7 @@ export const fetchProgressData = async ( return null; } }; + export const fetchDashboardConfig = async (): Promise => { 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("/api/dashboard/persons/"); +} diff --git a/server/config/urls.py b/server/config/urls.py index 59959ebb..b18dc421 100644 --- a/server/config/urls.py +++ b/server/config/urls.py @@ -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"), diff --git a/server/vbv_lernwelt/assignment/creators/create_assignments.py b/server/vbv_lernwelt/assignment/creators/create_assignments.py index 4b30c326..07875a7e 100644 --- a/server/vbv_lernwelt/assignment/creators/create_assignments.py +++ b/server/vbv_lernwelt/assignment/creators/create_assignments.py @@ -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( """

Ausgangslage

@@ -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( """ diff --git a/server/vbv_lernwelt/course/admin.py b/server/vbv_lernwelt/course/admin.py index e64c5ada..4a4741f3 100644 --- a/server/vbv_lernwelt/course/admin.py +++ b/server/vbv_lernwelt/course/admin.py @@ -23,6 +23,8 @@ class CourseConfigurationAdmin(admin.ModelAdmin): "enable_circle_documents", "enable_learning_mentor", "enable_competence_certificates", + "is_vv", + "is_uk", ] diff --git a/server/vbv_lernwelt/course/creators/test_course.py b/server/vbv_lernwelt/course/creators/test_course.py index f6532b6d..124f4146 100644 --- a/server/vbv_lernwelt/course/creators/test_course.py +++ b/server/vbv_lernwelt/course/creators/test_course.py @@ -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"

Lernmedien

" - f"

Allgemeines

" - f"
  • Mit Risiken im Strassenverkehr umgehen
  • Versicherungsschutz
  • Vertragsarten
  • Zusammenfassung
" + "

Lernmedien

" + "

Allgemeines

" + "
  • Mit Risiken im Strassenverkehr umgehen
  • Versicherungsschutz
  • Vertragsarten
  • Zusammenfassung
" ), ) @@ -778,9 +796,9 @@ def create_test_media_library(): title=cat, parent=media_lib_allgemeines, body=RichText( - f"

Lernmedien

" - f"

Allgemeines

" - f"
  • Mit Risiken im Strassenverkehr umgehen
  • Versicherungsschutz
  • Vertragsarten
  • Zusammenfassung
" + "

Lernmedien

" + "

Allgemeines

" + "
  • Mit Risiken im Strassenverkehr umgehen
  • Versicherungsschutz
  • Vertragsarten
  • Zusammenfassung
" ), ) diff --git a/server/vbv_lernwelt/course/management/commands/create_default_courses.py b/server/vbv_lernwelt/course/management/commands/create_default_courses.py index f9b296d8..0ba564a4 100644 --- a/server/vbv_lernwelt/course/management/commands/create_default_courses.py +++ b/server/vbv_lernwelt/course/management/commands/create_default_courses.py @@ -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() diff --git a/server/vbv_lernwelt/course/migrations/0008_auto_20240403_1132.py b/server/vbv_lernwelt/course/migrations/0008_auto_20240403_1132.py new file mode 100644 index 00000000..7e7c1e21 --- /dev/null +++ b/server/vbv_lernwelt/course/migrations/0008_auto_20240403_1132.py @@ -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), + ] diff --git a/server/vbv_lernwelt/course/models.py b/server/vbv_lernwelt/course/models.py index 47ed76e2..aa94f21b 100644 --- a/server/vbv_lernwelt/course/models.py +++ b/server/vbv_lernwelt/course/models.py @@ -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}'" diff --git a/server/vbv_lernwelt/course/serializers.py b/server/vbv_lernwelt/course/serializers.py index e88133cb..db35a0a6 100644 --- a/server/vbv_lernwelt/course/serializers.py +++ b/server/vbv_lernwelt/course/serializers.py @@ -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"] diff --git a/server/vbv_lernwelt/dashboard/tests/test_views.py b/server/vbv_lernwelt/dashboard/tests/test_views.py new file mode 100644 index 00000000..4f0a5c99 --- /dev/null +++ b/server/vbv_lernwelt/dashboard/tests/test_views.py @@ -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"}) diff --git a/server/vbv_lernwelt/dashboard/views.py b/server/vbv_lernwelt/dashboard/views.py new file mode 100644 index 00000000..0f44b479 --- /dev/null +++ b/server/vbv_lernwelt/dashboard/views.py @@ -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) diff --git a/server/vbv_lernwelt/iam/permissions.py b/server/vbv_lernwelt/iam/permissions.py index b2210c7c..b9b141b0 100644 --- a/server/vbv_lernwelt/iam/permissions.py +++ b/server/vbv_lernwelt/iam/permissions.py @@ -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, } ) diff --git a/server/vbv_lernwelt/iam/tests/test_actions.py b/server/vbv_lernwelt/iam/tests/test_actions.py index 01b586c3..84d70e5e 100644 --- a/server/vbv_lernwelt/iam/tests/test_actions.py +++ b/server/vbv_lernwelt/iam/tests/test_actions.py @@ -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",