From e13d72eb8a30b751e4c6098fb58bc5729d87ac1c Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Wed, 3 Apr 2024 07:10:13 +0200 Subject: [PATCH] Add dashboard persons api view --- server/config/urls.py | 10 +- .../assignment/creators/create_assignments.py | 18 ++- .../course/creators/test_course.py | 126 ++++++++++-------- .../commands/create_default_courses.py | 2 +- server/vbv_lernwelt/course/serializers.py | 9 +- .../dashboard/tests/test_views.py | 86 ++++++++++++ server/vbv_lernwelt/dashboard/views.py | 87 ++++++++++++ server/vbv_lernwelt/iam/tests/test_actions.py | 5 +- 8 files changed, 277 insertions(+), 66 deletions(-) create mode 100644 server/vbv_lernwelt/dashboard/tests/test_views.py create mode 100644 server/vbv_lernwelt/dashboard/views.py 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"" + "

Lernmedien

" + "

Allgemeines

" + "" ), ) @@ -778,9 +796,9 @@ def create_test_media_library(): title=cat, parent=media_lib_allgemeines, body=RichText( - f"

Lernmedien

" - f"

Allgemeines

" - f"" + "

Lernmedien

" + "

Allgemeines

" + "" ), ) 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",