Add dashboard persons api view

This commit is contained in:
Daniel Egger 2024-04-03 07:10:13 +02:00
parent 16ebd23edf
commit e13d72eb8a
8 changed files with 277 additions and 66 deletions

View File

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

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

@ -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

@ -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 (
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"})

View File

@ -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)

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