diff --git a/server/config/urls.py b/server/config/urls.py
index 59959ebb..9848a2c1 100644
--- a/server/config/urls.py
+++ b/server/config/urls.py
@@ -10,6 +10,9 @@ from django.views import defaults as default_views
from django.views.decorators.csrf import csrf_exempt
from django_ratelimit.exceptions import Ratelimited
from graphene_django.views import GraphQLView
+from wagtail import urls as wagtail_urls
+from wagtail.admin import urls as wagtailadmin_urls
+from wagtail.documents import urls as media_library_urls
from vbv_lernwelt.api.directory import list_entities
from vbv_lernwelt.api.user import get_profile, me_user_view, post_avatar
@@ -39,6 +42,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,
@@ -57,9 +61,6 @@ from vbv_lernwelt.importer.views import (
)
from vbv_lernwelt.media_files.views import user_image
from vbv_lernwelt.notify.views import email_notification_settings
-from wagtail import urls as wagtail_urls
-from wagtail.admin import urls as wagtailadmin_urls
-from wagtail.documents import urls as media_library_urls
class SignedIntConverter(IntConverter):
@@ -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/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/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..4f9bc491
--- /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 (
+ create_course,
+ create_course_session,
+ create_user,
+ add_course_session_user,
+ create_course_session_group,
+ add_course_session_group_supervisor,
+)
+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..1e2181c1
--- /dev/null
+++ b/server/vbv_lernwelt/dashboard/views.py
@@ -0,0 +1,87 @@
+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.serializers import CourseSessionSerializer
+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)
+ all_to_serialize = course_sessions
+
+ return Response(
+ status=200,
+ data=CourseSessionSerializer(
+ all_to_serialize, many=True, context={"user": request.user}
+ ).data,
+ )
+ 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/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",