From 90a8f851d221a1660d9002050e3a48d7dc602dfc Mon Sep 17 00:00:00 2001 From: Reto Aebersold Date: Mon, 16 Oct 2023 09:23:25 +0200 Subject: [PATCH 01/85] feat: add dashboard API --- server/vbv_lernwelt/core/schema.py | 2 + server/vbv_lernwelt/course/models.py | 1 + server/vbv_lernwelt/dashboard/__init__.py | 0 .../dashboard/graphql/__init__.py | 0 .../vbv_lernwelt/dashboard/graphql/queries.py | 20 ++++++ .../vbv_lernwelt/dashboard/graphql/types.py | 16 +++++ .../vbv_lernwelt/dashboard/tests/__init__.py | 0 .../dashboard/tests/test_graphql.py | 61 +++++++++++++++++++ server/vbv_lernwelt/dashboard/tests/utils.py | 42 +++++++++++++ 9 files changed, 142 insertions(+) create mode 100644 server/vbv_lernwelt/dashboard/__init__.py create mode 100644 server/vbv_lernwelt/dashboard/graphql/__init__.py create mode 100644 server/vbv_lernwelt/dashboard/graphql/queries.py create mode 100644 server/vbv_lernwelt/dashboard/graphql/types.py create mode 100644 server/vbv_lernwelt/dashboard/tests/__init__.py create mode 100644 server/vbv_lernwelt/dashboard/tests/test_graphql.py create mode 100644 server/vbv_lernwelt/dashboard/tests/utils.py diff --git a/server/vbv_lernwelt/core/schema.py b/server/vbv_lernwelt/core/schema.py index c336e533..be4de6f0 100644 --- a/server/vbv_lernwelt/core/schema.py +++ b/server/vbv_lernwelt/core/schema.py @@ -6,6 +6,7 @@ from vbv_lernwelt.competence.graphql.queries import CompetenceCertificateQuery from vbv_lernwelt.course.graphql.queries import CourseQuery from vbv_lernwelt.course_session.graphql.mutations import CourseSessionMutation from vbv_lernwelt.course_session.graphql.queries import CourseSessionQuery +from vbv_lernwelt.dashboard.graphql.queries import DashboardQuery from vbv_lernwelt.feedback.graphql.mutations import FeedbackMutation from vbv_lernwelt.learnpath.graphql.queries import LearningPathQuery @@ -16,6 +17,7 @@ class Query( CourseQuery, CourseSessionQuery, LearningPathQuery, + DashboardQuery, graphene.ObjectType, ): pass diff --git a/server/vbv_lernwelt/course/models.py b/server/vbv_lernwelt/course/models.py index 4b6b1c2d..f735998f 100644 --- a/server/vbv_lernwelt/course/models.py +++ b/server/vbv_lernwelt/course/models.py @@ -266,6 +266,7 @@ class CourseSessionUser(models.Model): class Role(models.TextChoices): MEMBER = "MEMBER", _("Teilnehmer") EXPERT = "EXPERT", _("Experte/Trainer") + SESSION_SUPERVISOR = "SESSION_SUPERVISOR", _("Regionalleiter") TUTOR = "TUTOR", _("Lernbegleitung") role = models.CharField(choices=Role.choices, max_length=255, default=Role.MEMBER) diff --git a/server/vbv_lernwelt/dashboard/__init__.py b/server/vbv_lernwelt/dashboard/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/server/vbv_lernwelt/dashboard/graphql/__init__.py b/server/vbv_lernwelt/dashboard/graphql/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/server/vbv_lernwelt/dashboard/graphql/queries.py b/server/vbv_lernwelt/dashboard/graphql/queries.py new file mode 100644 index 00000000..d245b20a --- /dev/null +++ b/server/vbv_lernwelt/dashboard/graphql/queries.py @@ -0,0 +1,20 @@ +import graphene + +from vbv_lernwelt.course.models import Course, CourseSessionUser +from vbv_lernwelt.dashboard.graphql.types import CourseDashboardType + + +class DashboardQuery(graphene.ObjectType): + course_dashboard = graphene.List(CourseDashboardType) + + def resolve_course_dashboard(root, info, course_id: str | None = None): + user = info.context.user + courses = Course.objects.filter( + coursesession__coursesessionuser__user=user, + coursesession__coursesessionuser__role=CourseSessionUser.Role.SESSION_SUPERVISOR, + ).distinct() + + return [ + CourseDashboardType(course_id=course.id, course_title=course.title) + for course in courses + ] diff --git a/server/vbv_lernwelt/dashboard/graphql/types.py b/server/vbv_lernwelt/dashboard/graphql/types.py new file mode 100644 index 00000000..e22c2489 --- /dev/null +++ b/server/vbv_lernwelt/dashboard/graphql/types.py @@ -0,0 +1,16 @@ +import graphene + + +class CourseSessionDashboardType(graphene.ObjectType): + session_id = graphene.String() + + +class CourseDashboardType(graphene.ObjectType): + course_id = graphene.String() + course_title = graphene.String() + course_sessions = graphene.List(CourseSessionDashboardType) + + def resolve_course_sessions(self, info): + session = [] + + return [CourseSessionDashboardType() for s in session] diff --git a/server/vbv_lernwelt/dashboard/tests/__init__.py b/server/vbv_lernwelt/dashboard/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/server/vbv_lernwelt/dashboard/tests/test_graphql.py b/server/vbv_lernwelt/dashboard/tests/test_graphql.py new file mode 100644 index 00000000..de399939 --- /dev/null +++ b/server/vbv_lernwelt/dashboard/tests/test_graphql.py @@ -0,0 +1,61 @@ +from graphene_django.utils import GraphQLTestCase + +from vbv_lernwelt.course.models import CourseSessionUser +from vbv_lernwelt.dashboard.tests.utils import ( + add_course_session_user, + create_course, + create_course_session, + create_user, +) + + +class DashboardTestCase(GraphQLTestCase): + GRAPHQL_URL = "/server/graphql/" + + def setUp(self): + self.trainer = create_user("supervisor") + self.course = create_course("Test Course") + self.course_session = create_course_session( + course=self.course, title="Test Bern 2022 a" + ) + add_course_session_user( + course_session=self.course_session, + user=self.trainer, + role=CourseSessionUser.Role.SESSION_SUPERVISOR, + ) + + def test_courses(self): + # GIVEN + some_course = create_course("Other Course") + some_course_session = create_course_session( + course=some_course, title="Here is go study" + ) + add_course_session_user( + course_session=some_course_session, + user=self.trainer, + role=CourseSessionUser.Role.MEMBER, + ) + + self.client.force_login(self.trainer) + + query = f""" + query {{ + course_dashboard {{ + course_id + course_sessions{{ + session_id + }} + }} + }} + """ + + # WHEN + response = self.query(query) + + # THEN + self.assertResponseNoErrors(response) + + course_dashboard = response.json()["data"]["course_dashboard"] + + self.assertEqual(len(course_dashboard), 1) + self.assertEqual(course_dashboard[0]["course_id"], str(self.course.id)) diff --git a/server/vbv_lernwelt/dashboard/tests/utils.py b/server/vbv_lernwelt/dashboard/tests/utils.py new file mode 100644 index 00000000..26f67be2 --- /dev/null +++ b/server/vbv_lernwelt/dashboard/tests/utils.py @@ -0,0 +1,42 @@ +from django.contrib.auth.hashers import make_password +from django.utils import timezone +from django.utils.text import slugify + +from vbv_lernwelt.core.models import User +from vbv_lernwelt.course.models import Course, CourseSession, CourseSessionUser + + +def create_course(title: str) -> Course: + return Course.objects.create( + title=title, slug=slugify(title), category_name="Handlungsfeld" + ) + + +def create_user(username: str) -> User: + user = User.objects.create_user( + username="username", + password=make_password("test"), + email=f"{username}@example.com", + language="de", + ) + + return user + + +def create_course_session(course: Course, title: str) -> CourseSession: + return CourseSession.objects.create( + course=course, + title=title, + import_id=title, + start_date=timezone.now(), + ) + + +def add_course_session_user( + course_session: CourseSession, user: User, role: CourseSessionUser.Role +) -> CourseSessionUser: + return CourseSessionUser.objects.create( + course_session=course_session, + user=user, + role=role, + ) From 7d691e4f781d9879f4cafcaac03873c647b2d64d Mon Sep 17 00:00:00 2001 From: Reto Aebersold Date: Mon, 16 Oct 2023 15:14:17 +0200 Subject: [PATCH 02/85] feat: dashboard attendance days --- .../vbv_lernwelt/dashboard/graphql/queries.py | 13 +- .../vbv_lernwelt/dashboard/graphql/types.py | 106 +++++++- .../dashboard/tests/test_graphql.py | 240 ++++++++++++++++-- server/vbv_lernwelt/dashboard/tests/utils.py | 78 +++++- 4 files changed, 397 insertions(+), 40 deletions(-) diff --git a/server/vbv_lernwelt/dashboard/graphql/queries.py b/server/vbv_lernwelt/dashboard/graphql/queries.py index d245b20a..ed398951 100644 --- a/server/vbv_lernwelt/dashboard/graphql/queries.py +++ b/server/vbv_lernwelt/dashboard/graphql/queries.py @@ -5,14 +5,21 @@ from vbv_lernwelt.dashboard.graphql.types import CourseDashboardType class DashboardQuery(graphene.ObjectType): - course_dashboard = graphene.List(CourseDashboardType) + course_dashboard = graphene.List( + CourseDashboardType, course_id=graphene.String(required=False) + ) def resolve_course_dashboard(root, info, course_id: str | None = None): user = info.context.user - courses = Course.objects.filter( + query = Course.objects.filter( coursesession__coursesessionuser__user=user, coursesession__coursesessionuser__role=CourseSessionUser.Role.SESSION_SUPERVISOR, - ).distinct() + ) + + if course_id: + query = query.filter(id=course_id) + + courses = query.distinct() return [ CourseDashboardType(course_id=course.id, course_title=course.title) diff --git a/server/vbv_lernwelt/dashboard/graphql/types.py b/server/vbv_lernwelt/dashboard/graphql/types.py index e22c2489..72fa81fd 100644 --- a/server/vbv_lernwelt/dashboard/graphql/types.py +++ b/server/vbv_lernwelt/dashboard/graphql/types.py @@ -1,16 +1,114 @@ +import datetime +import math +from typing import List + import graphene +from vbv_lernwelt.course.models import CourseSessionUser +from vbv_lernwelt.course_session.models import CourseSessionAttendanceCourse +from vbv_lernwelt.course_session.services.attendance import AttendanceUserStatus +from vbv_lernwelt.notify.email.email_services import format_swiss_datetime + + +class CircleDashboardType(graphene.ObjectType): + circle_id = graphene.String() + circle_title = graphene.String() + class CourseSessionDashboardType(graphene.ObjectType): session_id = graphene.String() + session_title = graphene.String() + session_generation = graphene.String() + + +class AttendanceSummary(graphene.ObjectType): + days_completed = graphene.Int() + participants_present = graphene.Int() + + +class Record(graphene.ObjectType): + course_session_id = graphene.String() + generation = graphene.String() + circle_id = graphene.String() + due_date = graphene.String() + participants_present = graphene.Int() + participants_total = graphene.Int() + cockpit_url = graphene.String() + + +class AttendanceDayPresences(graphene.ObjectType): + records = graphene.List(Record) + summary = graphene.Field(AttendanceSummary) + filter = graphene.String() + + +def calculate_avg_participation(records: List[Record]) -> float: + if not records: + return 0.0 + + total_ratio = 0.0 + for record in records: + if record.participants_total == 0: + continue + total_ratio += float(record.participants_present) / float( + record.participants_total + ) + + return math.ceil(total_ratio / len(records) * 100) class CourseDashboardType(graphene.ObjectType): course_id = graphene.String() course_title = graphene.String() - course_sessions = graphene.List(CourseSessionDashboardType) + # course_sessions = graphene.List(CourseSessionDashboardType) + attendance_day_presences = graphene.Field(AttendanceDayPresences) - def resolve_course_sessions(self, info): - session = [] + def resolve_attendance_day_presences(root, info): + user = info.context.user + completed = CourseSessionAttendanceCourse.objects.filter( + course_session__coursesessionuser__user=user, + course_session__coursesessionuser__role=CourseSessionUser.Role.SESSION_SUPERVISOR, + due_date__end__lt=datetime.datetime.now(), + ).order_by("-due_date__end") - return [CourseSessionDashboardType() for s in session] + _filter = {} + records = [] + + for attendance_day in completed: + circle = attendance_day.learning_content.get_parent_circle() + + course_session = attendance_day.course_session + + url = f"/course/{course_session.course.slug}/cockpit/attendance?id={attendance_day.learning_content.id}&courseSessionId={course_session.id}" + + participants_total = CourseSessionUser.objects.filter( + course_session=course_session, role=CourseSessionUser.Role.MEMBER + ).count() + + participants_present = len( + [ + participant + for participant in attendance_day.attendance_user_list + if participant["status"] == AttendanceUserStatus.PRESENT.value + ] + ) + + _filter[circle.id] = circle.title + records.append( + Record( + course_session_id=course_session.id, + generation=course_session.generation, + circle_id=circle.id, + due_date=format_swiss_datetime(attendance_day.due_date.end), + participants_present=participants_present, + participants_total=participants_total, + cockpit_url=url, + ) + ) + + summary = AttendanceSummary( + days_completed=completed.count(), + participants_present=calculate_avg_participation(records), + ) + + return AttendanceDayPresences(summary=summary, records=records, filter="fuck") diff --git a/server/vbv_lernwelt/dashboard/tests/test_graphql.py b/server/vbv_lernwelt/dashboard/tests/test_graphql.py index de399939..bbafaa6d 100644 --- a/server/vbv_lernwelt/dashboard/tests/test_graphql.py +++ b/server/vbv_lernwelt/dashboard/tests/test_graphql.py @@ -1,8 +1,14 @@ +from datetime import timedelta + +from django.utils import timezone from graphene_django.utils import GraphQLTestCase from vbv_lernwelt.course.models import CourseSessionUser +from vbv_lernwelt.course_session.services.attendance import AttendanceUserStatus from vbv_lernwelt.dashboard.tests.utils import ( add_course_session_user, + create_attendance_course, + create_circle, create_course, create_course_session, create_user, @@ -12,38 +18,209 @@ from vbv_lernwelt.dashboard.tests.utils import ( class DashboardTestCase(GraphQLTestCase): GRAPHQL_URL = "/server/graphql/" - def setUp(self): - self.trainer = create_user("supervisor") - self.course = create_course("Test Course") - self.course_session = create_course_session( - course=self.course, title="Test Bern 2022 a" - ) + # def test_course_dashboard(self): + # # GIVEN + # supervisor = create_user("supervisor") + # course = create_course("Test Course") + # course_session = create_course_session( + # course=course, title="Test Bern 2022 a" + # ) + # add_course_session_user( + # course_session=course_session, + # user=supervisor, + # role=CourseSessionUser.Role.SESSION_SUPERVISOR, + # ) + # + # some_course = create_course("Other Course") + # some_course_session = create_course_session( + # course=some_course, title="Here is go study" + # ) + # add_course_session_user( + # course_session=some_course_session, + # user=supervisor, + # role=CourseSessionUser.Role.MEMBER, + # ) + # + # self.client.force_login(supervisor) + # + # query = f""" + # query {{ + # course_dashboard {{ + # course_id + # course_title + # }} + # }} + # """ + # + # # WHEN + # response = self.query(query) + # + # # THEN + # self.assertResponseNoErrors(response) + # + # course_dashboard = response.json()["data"]["course_dashboard"] + # + # self.assertEqual(len(course_dashboard), 1) + # self.assertEqual(course_dashboard[0]["course_id"], str(course.id)) + # self.assertEqual(course_dashboard[0]["course_title"], str(course.title)) + # + # def test_course_dashboard_id(self): + # # GIVEN + # supervisor = create_user("supervisor") + # course_1 = create_course("Test Course 1") + # course_2 = create_course("Test Course 2") + # course_session_1 = create_course_session( + # course=course_1, title="Test Course 1 Session" + # ) + # course_session_2 = create_course_session( + # course=course_2, title="Test Course 2 Session" + # ) + # add_course_session_user( + # course_session=course_session_1, + # user=supervisor, + # role=CourseSessionUser.Role.SESSION_SUPERVISOR, + # ) + # add_course_session_user( + # course_session=course_session_2, + # user=supervisor, + # role=CourseSessionUser.Role.SESSION_SUPERVISOR, + # ) + # + # self.client.force_login(supervisor) + # + # query = f"""query($course_id: String) {{ + # course_dashboard(course_id: $course_id) {{ + # course_id + # }} + # }} + # """ + # variables = {"course_id": str(course_2.id)} + # + # # WHEN + # response = self.query(query, variables=variables) + # + # # THEN + # self.assertResponseNoErrors(response) + # + # course_dashboard = response.json()["data"]["course_dashboard"] + # + # self.assertEqual(len(course_dashboard), 1) + # self.assertEqual(course_dashboard[0]["course_id"], str(course_2.id)) + # + # def test_course_dashboard_sessions(self): + # # GIVEN + # supervisor = create_user("supervisor") + # course = create_course("Test Course") + # course_session = create_course_session( + # course=course, title="Test Bern 2022 a" + # ) + # add_course_session_user( + # course_session=course_session, + # user=supervisor, + # role=CourseSessionUser.Role.SESSION_SUPERVISOR, + # ) + # + # self.client.force_login(supervisor) + # + # query = f""" + # query {{ + # course_dashboard {{ + # course_sessions{{ + # session_id + # session_title + # session_generation + # }} + # }} + # }} + # """ + # + # # WHEN + # response = self.query(query) + # + # # THEN + # self.assertResponseNoErrors(response) + # + # course_dashboard = response.json()["data"]["course_dashboard"][0] + # session = course_dashboard["course_sessions"][0] + # self.assertEqual(session["session_id"], str(course_session.id)) + # self.assertEqual(session["session_title"], str(course_session.title)) + # self.assertEqual(session["session_generation"], str(course_session.generation)) + + def test_attendance_day_presences(self): + # GIVEN + course, course_page = create_course("Test Course") + course_session = create_course_session(course=course, title="Test Bern 2022 a") + + supervisor = create_user("supervisor") + add_course_session_user( - course_session=self.course_session, - user=self.trainer, + course_session=course_session, + user=supervisor, role=CourseSessionUser.Role.SESSION_SUPERVISOR, ) - def test_courses(self): - # GIVEN - some_course = create_course("Other Course") - some_course_session = create_course_session( - course=some_course, title="Here is go study" - ) + circle, _ = create_circle(title="Test Circle", course_page=course_page) + + m1 = create_user("member_1") add_course_session_user( - course_session=some_course_session, - user=self.trainer, + course_session=course_session, + user=m1, role=CourseSessionUser.Role.MEMBER, ) - self.client.force_login(self.trainer) + m2 = create_user("member_2") + add_course_session_user( + course_session=course_session, + user=m2, + role=CourseSessionUser.Role.MEMBER, + ) + + m3 = create_user("member_3") + add_course_session_user( + course_session=course_session, + user=m3, + role=CourseSessionUser.Role.MEMBER, + ) + + e1 = create_user("expert_1") + add_course_session_user( + course_session=course_session, + user=e1, + role=CourseSessionUser.Role.EXPERT, + ) + + attendance_user_list = [ + {"user_id": str(m1.id), "status": AttendanceUserStatus.PRESENT.value}, + {"user_id": str(m2.id), "status": AttendanceUserStatus.ABSENT.value}, + ] + + due_date_end = timezone.now() - timedelta(hours=2) + attendance_course = create_attendance_course( + course_session=course_session, + circle=circle, + attendance_user_list=attendance_user_list, + due_date_end=due_date_end, + ) + + self.client.force_login(supervisor) query = f""" query {{ course_dashboard {{ - course_id - course_sessions{{ - session_id + attendance_day_presences{{ + summary{{ + days_completed + participants_present + }} + records{{ + course_session_id + generation + circle_id + due_date + participants_present + participants_total + cockpit_url + }} }} }} }} @@ -52,10 +229,25 @@ class DashboardTestCase(GraphQLTestCase): # WHEN response = self.query(query) - # THEN self.assertResponseNoErrors(response) - course_dashboard = response.json()["data"]["course_dashboard"] + data = response.json()["data"] - self.assertEqual(len(course_dashboard), 1) - self.assertEqual(course_dashboard[0]["course_id"], str(self.course.id)) + attendance_day_presences = data["course_dashboard"][0][ + "attendance_day_presences" + ] + + record = attendance_day_presences["records"][0] + + self.assertEqual(record["course_session_id"], str(course_session.id)) + self.assertEqual(record["generation"], "2023") + self.assertEqual(record["participants_present"], 1) + self.assertEqual(record["participants_total"], 3) + self.assertEqual( + record["cockpit_url"], + f"/course/test-lehrgang/cockpit/attendance?id={attendance_course.learning_content.id}&courseSessionId={course_session.id}", + ) + + summary = attendance_day_presences["summary"] + self.assertEqual(summary["days_completed"], 1) + self.assertEqual(summary["participants_present"], 34) diff --git a/server/vbv_lernwelt/dashboard/tests/utils.py b/server/vbv_lernwelt/dashboard/tests/utils.py index 26f67be2..9a1cb7db 100644 --- a/server/vbv_lernwelt/dashboard/tests/utils.py +++ b/server/vbv_lernwelt/dashboard/tests/utils.py @@ -1,33 +1,57 @@ +from datetime import datetime, timedelta +from typing import List, Tuple + from django.contrib.auth.hashers import make_password from django.utils import timezone -from django.utils.text import slugify from vbv_lernwelt.core.models import User -from vbv_lernwelt.course.models import Course, CourseSession, CourseSessionUser +from vbv_lernwelt.course.factories import CoursePageFactory +from vbv_lernwelt.course.models import ( + Course, + CoursePage, + CourseSession, + CourseSessionUser, +) +from vbv_lernwelt.course.utils import get_wagtail_default_site +from vbv_lernwelt.course_session.models import CourseSessionAttendanceCourse +from vbv_lernwelt.duedate.models import DueDate +from vbv_lernwelt.learnpath.models import Circle, LearningPath +from vbv_lernwelt.learnpath.tests.learning_path_factories import ( + CircleFactory, + LearningContentAttendanceCourseFactory, + LearningPathFactory, +) -def create_course(title: str) -> Course: - return Course.objects.create( - title=title, slug=slugify(title), category_name="Handlungsfeld" +def create_course(title: str) -> Tuple[Course, CoursePage]: + course = Course.objects.create(title=title, category_name="Handlungsfeld") + + course_page = CoursePageFactory( + title="Test Lehrgang", + parent=get_wagtail_default_site().root_page, + course=course, ) + course.slug = course_page.slug + course.save() + + return course, course_page def create_user(username: str) -> User: - user = User.objects.create_user( - username="username", + return User.objects.create_user( + username=username, password=make_password("test"), email=f"{username}@example.com", language="de", ) - return user - def create_course_session(course: Course, title: str) -> CourseSession: return CourseSession.objects.create( course=course, title=title, import_id=title, + generation="2023", start_date=timezone.now(), ) @@ -40,3 +64,39 @@ def add_course_session_user( user=user, role=role, ) + + +def create_circle( + title: str, course_page: CoursePage, learning_path: LearningPath | None = None +) -> Tuple[Circle, LearningPath]: + if not learning_path: + learning_path = LearningPathFactory(title="Test Lernpfad", parent=course_page) + + circle = CircleFactory( + title=title, parent=learning_path, description="Circle Description" + ) + + return circle, learning_path + + +def create_attendance_course( + course_session: CourseSession, + circle: Circle, + attendance_user_list: List, + due_date_end: datetime, +) -> CourseSessionAttendanceCourse: + learning_content_dummy = LearningContentAttendanceCourseFactory( + title="Lerninhalt Dummy", + parent=circle, + ) + + return CourseSessionAttendanceCourse.objects.create( + course_session=course_session, + learning_content=learning_content_dummy, + attendance_user_list=attendance_user_list, + due_date=DueDate.objects.create( + course_session=course_session, + start=due_date_end - timedelta(hours=8), + end=due_date_end, + ), + ) From 9a6a897f3a57546064692fcac7be01138004d1ec Mon Sep 17 00:00:00 2001 From: Reto Aebersold Date: Mon, 16 Oct 2023 15:47:35 +0200 Subject: [PATCH 03/85] feat: dashboard API refactor --- .../vbv_lernwelt/dashboard/graphql/queries.py | 4 +- .../dashboard/graphql/types/__init__.py | 0 .../graphql/{types.py => types/attendance.py} | 17 +- .../dashboard/graphql/types/dashboard.py | 7 + .../dashboard/tests/graphql/__init__.py | 0 .../tests/graphql/test_attendance.py | 125 +++++++++ .../dashboard/tests/graphql/test_dashboard.py | 100 +++++++ .../dashboard/tests/{ => graphql}/utils.py | 0 .../dashboard/tests/test_graphql.py | 253 ------------------ 9 files changed, 237 insertions(+), 269 deletions(-) create mode 100644 server/vbv_lernwelt/dashboard/graphql/types/__init__.py rename server/vbv_lernwelt/dashboard/graphql/{types.py => types/attendance.py} (88%) create mode 100644 server/vbv_lernwelt/dashboard/graphql/types/dashboard.py create mode 100644 server/vbv_lernwelt/dashboard/tests/graphql/__init__.py create mode 100644 server/vbv_lernwelt/dashboard/tests/graphql/test_attendance.py create mode 100644 server/vbv_lernwelt/dashboard/tests/graphql/test_dashboard.py rename server/vbv_lernwelt/dashboard/tests/{ => graphql}/utils.py (100%) delete mode 100644 server/vbv_lernwelt/dashboard/tests/test_graphql.py diff --git a/server/vbv_lernwelt/dashboard/graphql/queries.py b/server/vbv_lernwelt/dashboard/graphql/queries.py index ed398951..ad6400f5 100644 --- a/server/vbv_lernwelt/dashboard/graphql/queries.py +++ b/server/vbv_lernwelt/dashboard/graphql/queries.py @@ -1,12 +1,12 @@ import graphene from vbv_lernwelt.course.models import Course, CourseSessionUser -from vbv_lernwelt.dashboard.graphql.types import CourseDashboardType +from vbv_lernwelt.dashboard.graphql.types.attendance import CourseDashboardType class DashboardQuery(graphene.ObjectType): course_dashboard = graphene.List( - CourseDashboardType, course_id=graphene.String(required=False) + CourseDashboardType, course_id=graphene.ID(required=False) ) def resolve_course_dashboard(root, info, course_id: str | None = None): diff --git a/server/vbv_lernwelt/dashboard/graphql/types/__init__.py b/server/vbv_lernwelt/dashboard/graphql/types/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/server/vbv_lernwelt/dashboard/graphql/types.py b/server/vbv_lernwelt/dashboard/graphql/types/attendance.py similarity index 88% rename from server/vbv_lernwelt/dashboard/graphql/types.py rename to server/vbv_lernwelt/dashboard/graphql/types/attendance.py index 72fa81fd..4afff8be 100644 --- a/server/vbv_lernwelt/dashboard/graphql/types.py +++ b/server/vbv_lernwelt/dashboard/graphql/types/attendance.py @@ -10,26 +10,15 @@ from vbv_lernwelt.course_session.services.attendance import AttendanceUserStatus from vbv_lernwelt.notify.email.email_services import format_swiss_datetime -class CircleDashboardType(graphene.ObjectType): - circle_id = graphene.String() - circle_title = graphene.String() - - -class CourseSessionDashboardType(graphene.ObjectType): - session_id = graphene.String() - session_title = graphene.String() - session_generation = graphene.String() - - class AttendanceSummary(graphene.ObjectType): days_completed = graphene.Int() participants_present = graphene.Int() class Record(graphene.ObjectType): - course_session_id = graphene.String() + course_session_id = graphene.ID() generation = graphene.String() - circle_id = graphene.String() + circle_id = graphene.ID() due_date = graphene.String() participants_present = graphene.Int() participants_total = graphene.Int() @@ -60,7 +49,6 @@ def calculate_avg_participation(records: List[Record]) -> float: class CourseDashboardType(graphene.ObjectType): course_id = graphene.String() course_title = graphene.String() - # course_sessions = graphene.List(CourseSessionDashboardType) attendance_day_presences = graphene.Field(AttendanceDayPresences) def resolve_attendance_day_presences(root, info): @@ -69,6 +57,7 @@ class CourseDashboardType(graphene.ObjectType): course_session__coursesessionuser__user=user, course_session__coursesessionuser__role=CourseSessionUser.Role.SESSION_SUPERVISOR, due_date__end__lt=datetime.datetime.now(), + course_session__course_id=root.course_id, ).order_by("-due_date__end") _filter = {} diff --git a/server/vbv_lernwelt/dashboard/graphql/types/dashboard.py b/server/vbv_lernwelt/dashboard/graphql/types/dashboard.py new file mode 100644 index 00000000..752c23f2 --- /dev/null +++ b/server/vbv_lernwelt/dashboard/graphql/types/dashboard.py @@ -0,0 +1,7 @@ +import graphene + + +class CourseSessionDashboardType(graphene.ObjectType): + session_id = graphene.ID() + session_title = graphene.String() + session_generation = graphene.String() diff --git a/server/vbv_lernwelt/dashboard/tests/graphql/__init__.py b/server/vbv_lernwelt/dashboard/tests/graphql/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/server/vbv_lernwelt/dashboard/tests/graphql/test_attendance.py b/server/vbv_lernwelt/dashboard/tests/graphql/test_attendance.py new file mode 100644 index 00000000..6c3d62f9 --- /dev/null +++ b/server/vbv_lernwelt/dashboard/tests/graphql/test_attendance.py @@ -0,0 +1,125 @@ +from datetime import timedelta + +from django.utils import timezone +from graphene_django.utils import GraphQLTestCase + +from vbv_lernwelt.course.models import CourseSessionUser +from vbv_lernwelt.course_session.services.attendance import AttendanceUserStatus +from vbv_lernwelt.dashboard.tests.graphql.utils import ( + add_course_session_user, + create_attendance_course, + create_circle, + create_course, + create_course_session, + create_user, +) + + +class DashboardTestCase(GraphQLTestCase): + GRAPHQL_URL = "/server/graphql/" + + def test_attendance_day_presences(self): + # GIVEN + course, course_page = create_course("Test Course") + course_session = create_course_session(course=course, title="Test Bern 2022 a") + + supervisor = create_user("supervisor") + + add_course_session_user( + course_session=course_session, + user=supervisor, + role=CourseSessionUser.Role.SESSION_SUPERVISOR, + ) + + circle, _ = create_circle(title="Test Circle", course_page=course_page) + + m1 = create_user("member_1") + add_course_session_user( + course_session=course_session, + user=m1, + role=CourseSessionUser.Role.MEMBER, + ) + + m2 = create_user("member_2") + add_course_session_user( + course_session=course_session, + user=m2, + role=CourseSessionUser.Role.MEMBER, + ) + + m3 = create_user("member_3") + add_course_session_user( + course_session=course_session, + user=m3, + role=CourseSessionUser.Role.MEMBER, + ) + + e1 = create_user("expert_1") + add_course_session_user( + course_session=course_session, + user=e1, + role=CourseSessionUser.Role.EXPERT, + ) + + attendance_user_list = [ + {"user_id": str(m1.id), "status": AttendanceUserStatus.PRESENT.value}, + {"user_id": str(m2.id), "status": AttendanceUserStatus.ABSENT.value}, + ] + + due_date_end = timezone.now() - timedelta(hours=2) + attendance_course = create_attendance_course( + course_session=course_session, + circle=circle, + attendance_user_list=attendance_user_list, + due_date_end=due_date_end, + ) + + self.client.force_login(supervisor) + + query = f""" + query($course_id: ID) {{ + course_dashboard(course_id: $course_id) {{ + attendance_day_presences{{ + summary{{ + days_completed + participants_present + }} + records{{ + course_session_id + generation + circle_id + due_date + participants_present + participants_total + cockpit_url + }} + }} + }} + }} + """ + + # WHEN + response = self.query(query, variables={"course_id": str(course.id)}) + + self.assertResponseNoErrors(response) + + data = response.json()["data"] + + attendance_day_presences = data["course_dashboard"][0][ + "attendance_day_presences" + ] + + record = attendance_day_presences["records"][0] + + self.assertEqual(record["course_session_id"], str(course_session.id)) + self.assertEqual(record["generation"], "2023") + self.assertEqual(record["participants_present"], 1) + self.assertEqual(record["participants_total"], 3) + self.assertEqual( + record["cockpit_url"], + f"/course/test-lehrgang/cockpit/attendance?id={attendance_course.learning_content.id}&courseSessionId={course_session.id}", + ) + + summary = attendance_day_presences["summary"] + self.assertEqual(summary["days_completed"], 1) + self.assertEqual(summary["participants_present"], 34) diff --git a/server/vbv_lernwelt/dashboard/tests/graphql/test_dashboard.py b/server/vbv_lernwelt/dashboard/tests/graphql/test_dashboard.py new file mode 100644 index 00000000..f22bd6c3 --- /dev/null +++ b/server/vbv_lernwelt/dashboard/tests/graphql/test_dashboard.py @@ -0,0 +1,100 @@ +from graphene_django.utils import GraphQLTestCase + +from vbv_lernwelt.course.models import CourseSessionUser +from vbv_lernwelt.dashboard.tests.graphql.utils import ( + add_course_session_user, + create_course, + create_course_session, + create_user, +) + + +class DashboardTestCase(GraphQLTestCase): + GRAPHQL_URL = "/server/graphql/" + + def test_course_dashboard(self): + # GIVEN + supervisor = create_user("supervisor") + course, _ = create_course("Test Course") + course_session = create_course_session(course=course, title="Test Bern 2022 a") + add_course_session_user( + course_session=course_session, + user=supervisor, + role=CourseSessionUser.Role.SESSION_SUPERVISOR, + ) + + some_course, _ = create_course("Other Course") + some_course_session = create_course_session( + course=some_course, title="Here is go study" + ) + add_course_session_user( + course_session=some_course_session, + user=supervisor, + role=CourseSessionUser.Role.MEMBER, + ) + + self.client.force_login(supervisor) + + query = f""" + query {{ + course_dashboard {{ + course_id + course_title + }} + }} + """ + + # WHEN + response = self.query(query) + + # THEN + self.assertResponseNoErrors(response) + + course_dashboard = response.json()["data"]["course_dashboard"] + + self.assertEqual(len(course_dashboard), 1) + self.assertEqual(course_dashboard[0]["course_id"], str(course.id)) + self.assertEqual(course_dashboard[0]["course_title"], str(course.title)) + + def test_course_dashboard_id(self): + # GIVEN + supervisor = create_user("supervisor") + course_1, _ = create_course("Test Course 1") + course_2, _ = create_course("Test Course 2") + course_session_1 = create_course_session( + course=course_1, title="Test Course 1 Session" + ) + course_session_2 = create_course_session( + course=course_2, title="Test Course 2 Session" + ) + add_course_session_user( + course_session=course_session_1, + user=supervisor, + role=CourseSessionUser.Role.SESSION_SUPERVISOR, + ) + add_course_session_user( + course_session=course_session_2, + user=supervisor, + role=CourseSessionUser.Role.SESSION_SUPERVISOR, + ) + + self.client.force_login(supervisor) + + query = f"""query($course_id: ID) {{ + course_dashboard(course_id: $course_id) {{ + course_id + }} + }} + """ + variables = {"course_id": str(course_2.id)} + + # WHEN + response = self.query(query, variables=variables) + + # THEN + self.assertResponseNoErrors(response) + + course_dashboard = response.json()["data"]["course_dashboard"] + + self.assertEqual(len(course_dashboard), 1) + self.assertEqual(course_dashboard[0]["course_id"], str(course_2.id)) diff --git a/server/vbv_lernwelt/dashboard/tests/utils.py b/server/vbv_lernwelt/dashboard/tests/graphql/utils.py similarity index 100% rename from server/vbv_lernwelt/dashboard/tests/utils.py rename to server/vbv_lernwelt/dashboard/tests/graphql/utils.py diff --git a/server/vbv_lernwelt/dashboard/tests/test_graphql.py b/server/vbv_lernwelt/dashboard/tests/test_graphql.py deleted file mode 100644 index bbafaa6d..00000000 --- a/server/vbv_lernwelt/dashboard/tests/test_graphql.py +++ /dev/null @@ -1,253 +0,0 @@ -from datetime import timedelta - -from django.utils import timezone -from graphene_django.utils import GraphQLTestCase - -from vbv_lernwelt.course.models import CourseSessionUser -from vbv_lernwelt.course_session.services.attendance import AttendanceUserStatus -from vbv_lernwelt.dashboard.tests.utils import ( - add_course_session_user, - create_attendance_course, - create_circle, - create_course, - create_course_session, - create_user, -) - - -class DashboardTestCase(GraphQLTestCase): - GRAPHQL_URL = "/server/graphql/" - - # def test_course_dashboard(self): - # # GIVEN - # supervisor = create_user("supervisor") - # course = create_course("Test Course") - # course_session = create_course_session( - # course=course, title="Test Bern 2022 a" - # ) - # add_course_session_user( - # course_session=course_session, - # user=supervisor, - # role=CourseSessionUser.Role.SESSION_SUPERVISOR, - # ) - # - # some_course = create_course("Other Course") - # some_course_session = create_course_session( - # course=some_course, title="Here is go study" - # ) - # add_course_session_user( - # course_session=some_course_session, - # user=supervisor, - # role=CourseSessionUser.Role.MEMBER, - # ) - # - # self.client.force_login(supervisor) - # - # query = f""" - # query {{ - # course_dashboard {{ - # course_id - # course_title - # }} - # }} - # """ - # - # # WHEN - # response = self.query(query) - # - # # THEN - # self.assertResponseNoErrors(response) - # - # course_dashboard = response.json()["data"]["course_dashboard"] - # - # self.assertEqual(len(course_dashboard), 1) - # self.assertEqual(course_dashboard[0]["course_id"], str(course.id)) - # self.assertEqual(course_dashboard[0]["course_title"], str(course.title)) - # - # def test_course_dashboard_id(self): - # # GIVEN - # supervisor = create_user("supervisor") - # course_1 = create_course("Test Course 1") - # course_2 = create_course("Test Course 2") - # course_session_1 = create_course_session( - # course=course_1, title="Test Course 1 Session" - # ) - # course_session_2 = create_course_session( - # course=course_2, title="Test Course 2 Session" - # ) - # add_course_session_user( - # course_session=course_session_1, - # user=supervisor, - # role=CourseSessionUser.Role.SESSION_SUPERVISOR, - # ) - # add_course_session_user( - # course_session=course_session_2, - # user=supervisor, - # role=CourseSessionUser.Role.SESSION_SUPERVISOR, - # ) - # - # self.client.force_login(supervisor) - # - # query = f"""query($course_id: String) {{ - # course_dashboard(course_id: $course_id) {{ - # course_id - # }} - # }} - # """ - # variables = {"course_id": str(course_2.id)} - # - # # WHEN - # response = self.query(query, variables=variables) - # - # # THEN - # self.assertResponseNoErrors(response) - # - # course_dashboard = response.json()["data"]["course_dashboard"] - # - # self.assertEqual(len(course_dashboard), 1) - # self.assertEqual(course_dashboard[0]["course_id"], str(course_2.id)) - # - # def test_course_dashboard_sessions(self): - # # GIVEN - # supervisor = create_user("supervisor") - # course = create_course("Test Course") - # course_session = create_course_session( - # course=course, title="Test Bern 2022 a" - # ) - # add_course_session_user( - # course_session=course_session, - # user=supervisor, - # role=CourseSessionUser.Role.SESSION_SUPERVISOR, - # ) - # - # self.client.force_login(supervisor) - # - # query = f""" - # query {{ - # course_dashboard {{ - # course_sessions{{ - # session_id - # session_title - # session_generation - # }} - # }} - # }} - # """ - # - # # WHEN - # response = self.query(query) - # - # # THEN - # self.assertResponseNoErrors(response) - # - # course_dashboard = response.json()["data"]["course_dashboard"][0] - # session = course_dashboard["course_sessions"][0] - # self.assertEqual(session["session_id"], str(course_session.id)) - # self.assertEqual(session["session_title"], str(course_session.title)) - # self.assertEqual(session["session_generation"], str(course_session.generation)) - - def test_attendance_day_presences(self): - # GIVEN - course, course_page = create_course("Test Course") - course_session = create_course_session(course=course, title="Test Bern 2022 a") - - supervisor = create_user("supervisor") - - add_course_session_user( - course_session=course_session, - user=supervisor, - role=CourseSessionUser.Role.SESSION_SUPERVISOR, - ) - - circle, _ = create_circle(title="Test Circle", course_page=course_page) - - m1 = create_user("member_1") - add_course_session_user( - course_session=course_session, - user=m1, - role=CourseSessionUser.Role.MEMBER, - ) - - m2 = create_user("member_2") - add_course_session_user( - course_session=course_session, - user=m2, - role=CourseSessionUser.Role.MEMBER, - ) - - m3 = create_user("member_3") - add_course_session_user( - course_session=course_session, - user=m3, - role=CourseSessionUser.Role.MEMBER, - ) - - e1 = create_user("expert_1") - add_course_session_user( - course_session=course_session, - user=e1, - role=CourseSessionUser.Role.EXPERT, - ) - - attendance_user_list = [ - {"user_id": str(m1.id), "status": AttendanceUserStatus.PRESENT.value}, - {"user_id": str(m2.id), "status": AttendanceUserStatus.ABSENT.value}, - ] - - due_date_end = timezone.now() - timedelta(hours=2) - attendance_course = create_attendance_course( - course_session=course_session, - circle=circle, - attendance_user_list=attendance_user_list, - due_date_end=due_date_end, - ) - - self.client.force_login(supervisor) - - query = f""" - query {{ - course_dashboard {{ - attendance_day_presences{{ - summary{{ - days_completed - participants_present - }} - records{{ - course_session_id - generation - circle_id - due_date - participants_present - participants_total - cockpit_url - }} - }} - }} - }} - """ - - # WHEN - response = self.query(query) - - self.assertResponseNoErrors(response) - - data = response.json()["data"] - - attendance_day_presences = data["course_dashboard"][0][ - "attendance_day_presences" - ] - - record = attendance_day_presences["records"][0] - - self.assertEqual(record["course_session_id"], str(course_session.id)) - self.assertEqual(record["generation"], "2023") - self.assertEqual(record["participants_present"], 1) - self.assertEqual(record["participants_total"], 3) - self.assertEqual( - record["cockpit_url"], - f"/course/test-lehrgang/cockpit/attendance?id={attendance_course.learning_content.id}&courseSessionId={course_session.id}", - ) - - summary = attendance_day_presences["summary"] - self.assertEqual(summary["days_completed"], 1) - self.assertEqual(summary["participants_present"], 34) From ea2e303592ac8f591fa0855f29b26d04659c9eec Mon Sep 17 00:00:00 2001 From: Reto Aebersold Date: Tue, 17 Oct 2023 10:14:22 +0200 Subject: [PATCH 04/85] feat: dashboard API feedback --- .../vbv_lernwelt/dashboard/graphql/queries.py | 2 +- .../dashboard/graphql/types/attendance.py | 109 +++++++++--------- .../dashboard/graphql/types/dashboard.py | 16 +++ .../dashboard/graphql/types/feedback.py | 18 +++ 4 files changed, 87 insertions(+), 58 deletions(-) create mode 100644 server/vbv_lernwelt/dashboard/graphql/types/feedback.py diff --git a/server/vbv_lernwelt/dashboard/graphql/queries.py b/server/vbv_lernwelt/dashboard/graphql/queries.py index ad6400f5..4693c49c 100644 --- a/server/vbv_lernwelt/dashboard/graphql/queries.py +++ b/server/vbv_lernwelt/dashboard/graphql/queries.py @@ -1,7 +1,7 @@ import graphene from vbv_lernwelt.course.models import Course, CourseSessionUser -from vbv_lernwelt.dashboard.graphql.types.attendance import CourseDashboardType +from vbv_lernwelt.dashboard.graphql.types.dashboard import CourseDashboardType class DashboardQuery(graphene.ObjectType): diff --git a/server/vbv_lernwelt/dashboard/graphql/types/attendance.py b/server/vbv_lernwelt/dashboard/graphql/types/attendance.py index 4afff8be..3800c565 100644 --- a/server/vbv_lernwelt/dashboard/graphql/types/attendance.py +++ b/server/vbv_lernwelt/dashboard/graphql/types/attendance.py @@ -4,6 +4,7 @@ from typing import List import graphene +from vbv_lernwelt.core.models import User from vbv_lernwelt.course.models import CourseSessionUser from vbv_lernwelt.course_session.models import CourseSessionAttendanceCourse from vbv_lernwelt.course_session.services.attendance import AttendanceUserStatus @@ -31,6 +32,57 @@ class AttendanceDayPresences(graphene.ObjectType): filter = graphene.String() +def attendance_day_presences(course_id: graphene.String(), user: User): + completed = CourseSessionAttendanceCourse.objects.filter( + course_session__coursesessionuser__user=user, + course_session__coursesessionuser__role=CourseSessionUser.Role.SESSION_SUPERVISOR, + due_date__end__lt=datetime.datetime.now(), + course_session__course_id=course_id, + ).order_by("-due_date__end") + + _filter = {} + records = [] + + for attendance_day in completed: + circle = attendance_day.learning_content.get_parent_circle() + + course_session = attendance_day.course_session + + url = f"/course/{course_session.course.slug}/cockpit/attendance?id={attendance_day.learning_content.id}&courseSessionId={course_session.id}" + + participants_total = CourseSessionUser.objects.filter( + course_session=course_session, role=CourseSessionUser.Role.MEMBER + ).count() + + participants_present = len( + [ + participant + for participant in attendance_day.attendance_user_list + if participant["status"] == AttendanceUserStatus.PRESENT.value + ] + ) + + _filter[circle.id] = circle.title + records.append( + Record( + course_session_id=course_session.id, + generation=course_session.generation, + circle_id=circle.id, + due_date=format_swiss_datetime(attendance_day.due_date.end), + participants_present=participants_present, + participants_total=participants_total, + cockpit_url=url, + ) + ) + + summary = AttendanceSummary( + days_completed=completed.count(), + participants_present=calculate_avg_participation(records), + ) + + return AttendanceDayPresences(summary=summary, records=records, filter="fuck") + + def calculate_avg_participation(records: List[Record]) -> float: if not records: return 0.0 @@ -44,60 +96,3 @@ def calculate_avg_participation(records: List[Record]) -> float: ) return math.ceil(total_ratio / len(records) * 100) - - -class CourseDashboardType(graphene.ObjectType): - course_id = graphene.String() - course_title = graphene.String() - attendance_day_presences = graphene.Field(AttendanceDayPresences) - - def resolve_attendance_day_presences(root, info): - user = info.context.user - completed = CourseSessionAttendanceCourse.objects.filter( - course_session__coursesessionuser__user=user, - course_session__coursesessionuser__role=CourseSessionUser.Role.SESSION_SUPERVISOR, - due_date__end__lt=datetime.datetime.now(), - course_session__course_id=root.course_id, - ).order_by("-due_date__end") - - _filter = {} - records = [] - - for attendance_day in completed: - circle = attendance_day.learning_content.get_parent_circle() - - course_session = attendance_day.course_session - - url = f"/course/{course_session.course.slug}/cockpit/attendance?id={attendance_day.learning_content.id}&courseSessionId={course_session.id}" - - participants_total = CourseSessionUser.objects.filter( - course_session=course_session, role=CourseSessionUser.Role.MEMBER - ).count() - - participants_present = len( - [ - participant - for participant in attendance_day.attendance_user_list - if participant["status"] == AttendanceUserStatus.PRESENT.value - ] - ) - - _filter[circle.id] = circle.title - records.append( - Record( - course_session_id=course_session.id, - generation=course_session.generation, - circle_id=circle.id, - due_date=format_swiss_datetime(attendance_day.due_date.end), - participants_present=participants_present, - participants_total=participants_total, - cockpit_url=url, - ) - ) - - summary = AttendanceSummary( - days_completed=completed.count(), - participants_present=calculate_avg_participation(records), - ) - - return AttendanceDayPresences(summary=summary, records=records, filter="fuck") diff --git a/server/vbv_lernwelt/dashboard/graphql/types/dashboard.py b/server/vbv_lernwelt/dashboard/graphql/types/dashboard.py index 752c23f2..fb46c300 100644 --- a/server/vbv_lernwelt/dashboard/graphql/types/dashboard.py +++ b/server/vbv_lernwelt/dashboard/graphql/types/dashboard.py @@ -1,7 +1,23 @@ import graphene +from vbv_lernwelt.dashboard.graphql.types.attendance import ( + attendance_day_presences, + AttendanceDayPresences, +) +from vbv_lernwelt.dashboard.graphql.types.feedback import FeedbackResponses + class CourseSessionDashboardType(graphene.ObjectType): session_id = graphene.ID() session_title = graphene.String() session_generation = graphene.String() + + +class CourseDashboardType(graphene.ObjectType): + course_id = graphene.String() + course_title = graphene.String() + attendance_day_presences = graphene.Field(AttendanceDayPresences) + feedback_responses = graphene.Field(FeedbackResponses) + + def resolve_attendance_day_presences(root, info): + return attendance_day_presences(root.course_id, info.context.user) diff --git a/server/vbv_lernwelt/dashboard/graphql/types/feedback.py b/server/vbv_lernwelt/dashboard/graphql/types/feedback.py new file mode 100644 index 00000000..17eeb2e6 --- /dev/null +++ b/server/vbv_lernwelt/dashboard/graphql/types/feedback.py @@ -0,0 +1,18 @@ +import graphene + + +class FeedbackSummary(graphene.ObjectType): + satisfaction_average = graphene.Float() + satisfaction_total = graphene.Int() + total_responses = graphene.Int() + + +class Record(graphene.ObjectType): + course_session_id = graphene.ID() + generation = graphene.String() + circle_id = graphene.ID() + + +class FeedbackResponses(graphene.ObjectType): + records = graphene.List(Record) + summary = graphene.Field(FeedbackSummary) From 09181d0e72a847096d3916467fa38a705a2d36a4 Mon Sep 17 00:00:00 2001 From: Reto Aebersold Date: Wed, 18 Oct 2023 18:22:23 +0200 Subject: [PATCH 05/85] feat: dashboard API feedback --- .../dashboard/graphql/types/attendance.py | 5 +- .../dashboard/graphql/types/dashboard.py | 28 +++++++- .../dashboard/graphql/types/feedback.py | 47 +++++++++++++ .../tests/graphql/test_attendance.py | 2 +- .../dashboard/tests/graphql/test_feedback.py | 66 +++++++++++++++++++ 5 files changed, 140 insertions(+), 8 deletions(-) create mode 100644 server/vbv_lernwelt/dashboard/tests/graphql/test_feedback.py diff --git a/server/vbv_lernwelt/dashboard/graphql/types/attendance.py b/server/vbv_lernwelt/dashboard/graphql/types/attendance.py index 3800c565..66e38dfa 100644 --- a/server/vbv_lernwelt/dashboard/graphql/types/attendance.py +++ b/server/vbv_lernwelt/dashboard/graphql/types/attendance.py @@ -29,7 +29,6 @@ class Record(graphene.ObjectType): class AttendanceDayPresences(graphene.ObjectType): records = graphene.List(Record) summary = graphene.Field(AttendanceSummary) - filter = graphene.String() def attendance_day_presences(course_id: graphene.String(), user: User): @@ -40,7 +39,6 @@ def attendance_day_presences(course_id: graphene.String(), user: User): course_session__course_id=course_id, ).order_by("-due_date__end") - _filter = {} records = [] for attendance_day in completed: @@ -62,7 +60,6 @@ def attendance_day_presences(course_id: graphene.String(), user: User): ] ) - _filter[circle.id] = circle.title records.append( Record( course_session_id=course_session.id, @@ -80,7 +77,7 @@ def attendance_day_presences(course_id: graphene.String(), user: User): participants_present=calculate_avg_participation(records), ) - return AttendanceDayPresences(summary=summary, records=records, filter="fuck") + return AttendanceDayPresences(summary=summary, records=records) def calculate_avg_participation(records: List[Record]) -> float: diff --git a/server/vbv_lernwelt/dashboard/graphql/types/dashboard.py b/server/vbv_lernwelt/dashboard/graphql/types/dashboard.py index fb46c300..7013f504 100644 --- a/server/vbv_lernwelt/dashboard/graphql/types/dashboard.py +++ b/server/vbv_lernwelt/dashboard/graphql/types/dashboard.py @@ -4,20 +4,42 @@ from vbv_lernwelt.dashboard.graphql.types.attendance import ( attendance_day_presences, AttendanceDayPresences, ) -from vbv_lernwelt.dashboard.graphql.types.feedback import FeedbackResponses +from vbv_lernwelt.dashboard.graphql.types.feedback import ( + feedback_responses, + FeedbackResponses, +) -class CourseSessionDashboardType(graphene.ObjectType): +class CourseSessionData(graphene.ObjectType): session_id = graphene.ID() session_title = graphene.String() - session_generation = graphene.String() + + +class CircleData(graphene.ObjectType): + circle_id = graphene.ID() + circle_title = graphene.String() + experts = graphene.List(graphene.String) + + +class CourseSessionProperties(graphene.ObjectType): + sessions = graphene.List(CourseSessionData) + generations = graphene.List(graphene.String) + circles = graphene.List(CircleData) class CourseDashboardType(graphene.ObjectType): course_id = graphene.String() course_title = graphene.String() + course_session_properties = graphene.Field(CourseSessionProperties) attendance_day_presences = graphene.Field(AttendanceDayPresences) feedback_responses = graphene.Field(FeedbackResponses) def resolve_attendance_day_presences(root, info): return attendance_day_presences(root.course_id, info.context.user) + + def resolve_feedback_responses(root, info): + return feedback_responses(root.course_id, info.context.user) + + def resolve_course_session_properties(root, info): + pass + # return course_session_properties(root.course_id, info.context.user) diff --git a/server/vbv_lernwelt/dashboard/graphql/types/feedback.py b/server/vbv_lernwelt/dashboard/graphql/types/feedback.py index 17eeb2e6..d1704194 100644 --- a/server/vbv_lernwelt/dashboard/graphql/types/feedback.py +++ b/server/vbv_lernwelt/dashboard/graphql/types/feedback.py @@ -1,5 +1,11 @@ +from typing import List + import graphene +from vbv_lernwelt.core.models import User +from vbv_lernwelt.course.models import CourseSession, CourseSessionUser +from vbv_lernwelt.feedback.models import FeedbackResponse + class FeedbackSummary(graphene.ObjectType): satisfaction_average = graphene.Float() @@ -16,3 +22,44 @@ class Record(graphene.ObjectType): class FeedbackResponses(graphene.ObjectType): records = graphene.List(Record) summary = graphene.Field(FeedbackSummary) + + +def feedback_responses(course_id: graphene.String(), user: User): + # Get all course sessions for this user in the given course + course_sessions = CourseSession.objects.filter( + coursesessionuser__user=user, + coursesessionuser__role=CourseSessionUser.Role.SESSION_SUPERVISOR, + course_id=course_id, + ) + + for course_session in course_sessions: + fbs = FeedbackResponse.objects.filter( + submitted=True, + course_session=course_session, + # Only get feedbacks from members + feedback_user__in=CourseSessionUser.objects.filter( + course_session=course_session, role=CourseSessionUser.Role.MEMBER + ).values_list("user", flat=True), + ) + circle_feedbacks = circle_feedback_average(fbs) + + return FeedbackResponses() + + +def circle_feedback_average(feedbacks: List[FeedbackResponse]): + circle_data = {} + + for fb in feedbacks: + circle_id = fb.circle.id + satisfaction = fb.data.get("satisfaction", 0) + + if circle_id in circle_data: + circle_data[circle_id]["total"] += satisfaction + circle_data[circle_id]["count"] += 1 + else: + circle_data[circle_id] = {"total": satisfaction, "count": 1} + + for circle_id, data in circle_data.items(): + circle_data[circle_id]["avg_satisfaction"] = data["total"] / data["count"] + + return circle_data diff --git a/server/vbv_lernwelt/dashboard/tests/graphql/test_attendance.py b/server/vbv_lernwelt/dashboard/tests/graphql/test_attendance.py index 6c3d62f9..e6ddc8ba 100644 --- a/server/vbv_lernwelt/dashboard/tests/graphql/test_attendance.py +++ b/server/vbv_lernwelt/dashboard/tests/graphql/test_attendance.py @@ -15,7 +15,7 @@ from vbv_lernwelt.dashboard.tests.graphql.utils import ( ) -class DashboardTestCase(GraphQLTestCase): +class DashboardAttendanceTestCase(GraphQLTestCase): GRAPHQL_URL = "/server/graphql/" def test_attendance_day_presences(self): diff --git a/server/vbv_lernwelt/dashboard/tests/graphql/test_feedback.py b/server/vbv_lernwelt/dashboard/tests/graphql/test_feedback.py new file mode 100644 index 00000000..ccae60a2 --- /dev/null +++ b/server/vbv_lernwelt/dashboard/tests/graphql/test_feedback.py @@ -0,0 +1,66 @@ +from graphene_django.utils import GraphQLTestCase + +from vbv_lernwelt.course.models import CourseSessionUser +from vbv_lernwelt.dashboard.graphql.types.feedback import feedback_responses +from vbv_lernwelt.dashboard.tests.graphql.utils import ( + add_course_session_user, + create_circle, + create_course, + create_course_session, + create_user, +) +from vbv_lernwelt.feedback.models import FeedbackResponse + + +class DashboardFeedbackTestCase(GraphQLTestCase): + GRAPHQL_URL = "/server/graphql/" + + def test_feedback(self): + # GIVEN + course, course_page = create_course("Test Course") + course_session = create_course_session(course=course, title="Test Bern 2022 a") + + supervisor = create_user("supervisor") + + add_course_session_user( + course_session=course_session, + user=supervisor, + role=CourseSessionUser.Role.SESSION_SUPERVISOR, + ) + + member = create_user("member") + + circle1, _ = create_circle(title="Test Circle 1", course_page=course_page) + circle2, _ = create_circle(title="Test Circle 2", course_page=course_page) + + FeedbackResponse.objects.create( + feedback_user=member, + data={"satisfaction": 3}, + circle=circle1, + course_session=course_session, + ) + FeedbackResponse.objects.create( + feedback_user=member, + data={"satisfaction": 4}, + circle=circle1, + course_session=course_session, + ) + + # Create Feedbacks for circle2 + FeedbackResponse.objects.create( + feedback_user=member, + data={"satisfaction": 5}, + circle=circle2, + course_session=course_session, + ) + FeedbackResponse.objects.create( + feedback_user=member, + data={"satisfaction": 2}, + circle=circle2, + course_session=course_session, + ) + + # Get average satisfaction per circle + result = feedback_responses(course.id, supervisor) + + # THEN From 3925d074ee2f5d6ddc86e5e7a3db39292aa4b9d4 Mon Sep 17 00:00:00 2001 From: Reto Aebersold Date: Thu, 19 Oct 2023 09:48:16 +0200 Subject: [PATCH 06/85] feat: add course session props --- .../dashboard/graphql/types/dashboard.py | 53 ++++++++++++++++++- .../dashboard/tests/graphql/test_dashboard.py | 48 ++++++++++++++++- .../dashboard/tests/graphql/utils.py | 5 ++ 3 files changed, 103 insertions(+), 3 deletions(-) diff --git a/server/vbv_lernwelt/dashboard/graphql/types/dashboard.py b/server/vbv_lernwelt/dashboard/graphql/types/dashboard.py index 7013f504..863d7fe7 100644 --- a/server/vbv_lernwelt/dashboard/graphql/types/dashboard.py +++ b/server/vbv_lernwelt/dashboard/graphql/types/dashboard.py @@ -1,5 +1,6 @@ import graphene +from vbv_lernwelt.course.models import CourseSession, CourseSessionUser from vbv_lernwelt.dashboard.graphql.types.attendance import ( attendance_day_presences, AttendanceDayPresences, @@ -8,6 +9,7 @@ from vbv_lernwelt.dashboard.graphql.types.feedback import ( feedback_responses, FeedbackResponses, ) +from vbv_lernwelt.learnpath.models import Circle class CourseSessionData(graphene.ObjectType): @@ -41,5 +43,52 @@ class CourseDashboardType(graphene.ObjectType): return feedback_responses(root.course_id, info.context.user) def resolve_course_session_properties(root, info): - pass - # return course_session_properties(root.course_id, info.context.user) + course_session_data = [] + circle_data = [] + generations = set() + + course_sessions = CourseSession.objects.filter( + coursesessionuser__user=info.context.user, + coursesessionuser__role=CourseSessionUser.Role.SESSION_SUPERVISOR, + course_id=root.course_id, + ) + + for course_session in course_sessions: + course_session_data.append( + CourseSessionData( + session_id=course_session.id, + session_title=course_session.title, + ) + ) + generations.add(course_session.generation) + + # Get all circles for this course session + siblings = ( + course_session.course.get_learning_path() + .get_descendants() + .live() + .specific() + ) + + circles = [] + for sibling in siblings: + if sibling.specific_class == Circle: + circles.append(sibling.specific) + + for circle in circles: + circle_data.append( + CircleData( + circle_id=circle.id, + circle_title=circle.title, + experts=[ + f"{su.user.first_name} {su.user.last_name}" + for su in circle.expert.all() + ], + ) + ) + + return CourseSessionProperties( + sessions=course_session_data, + generations=list(generations), + circles=circle_data, + ) diff --git a/server/vbv_lernwelt/dashboard/tests/graphql/test_dashboard.py b/server/vbv_lernwelt/dashboard/tests/graphql/test_dashboard.py index f22bd6c3..4f305615 100644 --- a/server/vbv_lernwelt/dashboard/tests/graphql/test_dashboard.py +++ b/server/vbv_lernwelt/dashboard/tests/graphql/test_dashboard.py @@ -3,6 +3,7 @@ from graphene_django.utils import GraphQLTestCase from vbv_lernwelt.course.models import CourseSessionUser from vbv_lernwelt.dashboard.tests.graphql.utils import ( add_course_session_user, + create_circle, create_course, create_course_session, create_user, @@ -15,7 +16,7 @@ class DashboardTestCase(GraphQLTestCase): def test_course_dashboard(self): # GIVEN supervisor = create_user("supervisor") - course, _ = create_course("Test Course") + course, course_page = create_course("Test Course") course_session = create_course_session(course=course, title="Test Bern 2022 a") add_course_session_user( course_session=course_session, @@ -33,6 +34,17 @@ class DashboardTestCase(GraphQLTestCase): role=CourseSessionUser.Role.MEMBER, ) + circle, _ = create_circle(title="Test Circle", course_page=course_page) + + # expert + expert = create_user("expert") + expert_session_user = add_course_session_user( + course_session=some_course_session, + user=expert, + role=CourseSessionUser.Role.EXPERT, + ) + expert_session_user.expert.add(circle) + self.client.force_login(supervisor) query = f""" @@ -40,6 +52,18 @@ class DashboardTestCase(GraphQLTestCase): course_dashboard {{ course_id course_title + course_session_properties{{ + sessions{{ + session_id + session_title + }} + generations + circles{{ + circle_id + circle_title + experts + }} + }} }} }} """ @@ -56,6 +80,28 @@ class DashboardTestCase(GraphQLTestCase): self.assertEqual(course_dashboard[0]["course_id"], str(course.id)) self.assertEqual(course_dashboard[0]["course_title"], str(course.title)) + session_properties = course_dashboard[0]["course_session_properties"] + self.assertEqual(len(session_properties["sessions"]), 1) + self.assertEqual( + session_properties["sessions"][0]["session_id"], str(course_session.id) + ) + self.assertEqual( + session_properties["sessions"][0]["session_title"], + str(course_session.title), + ) + + self.assertEqual(len(session_properties["generations"]), 1) + self.assertEqual( + session_properties["generations"][0], str(course_session.generation) + ) + + self.assertEqual(len(session_properties["circles"]), 1) + self.assertEqual(session_properties["circles"][0]["circle_id"], str(circle.id)) + self.assertEqual( + session_properties["circles"][0]["circle_title"], str(circle.title) + ) + self.assertEqual(session_properties["circles"][0]["experts"], ["Test Expert"]) + def test_course_dashboard_id(self): # GIVEN supervisor = create_user("supervisor") diff --git a/server/vbv_lernwelt/dashboard/tests/graphql/utils.py b/server/vbv_lernwelt/dashboard/tests/graphql/utils.py index 9a1cb7db..9e757d78 100644 --- a/server/vbv_lernwelt/dashboard/tests/graphql/utils.py +++ b/server/vbv_lernwelt/dashboard/tests/graphql/utils.py @@ -20,6 +20,7 @@ from vbv_lernwelt.learnpath.tests.learning_path_factories import ( CircleFactory, LearningContentAttendanceCourseFactory, LearningPathFactory, + TopicFactory, ) @@ -43,6 +44,8 @@ def create_user(username: str) -> User: password=make_password("test"), email=f"{username}@example.com", language="de", + first_name="Test", + last_name=username.capitalize(), ) @@ -72,6 +75,8 @@ def create_circle( if not learning_path: learning_path = LearningPathFactory(title="Test Lernpfad", parent=course_page) + TopicFactory(title="Circle Test Topic", is_visible=False, parent=learning_path) + circle = CircleFactory( title=title, parent=learning_path, description="Circle Description" ) From 577746bff1ad87e75d9caef9eae33ba0c481391b Mon Sep 17 00:00:00 2001 From: Reto Aebersold Date: Thu, 19 Oct 2023 15:52:37 +0200 Subject: [PATCH 07/85] fix: filter feedback users --- .../dashboard/graphql/types/dashboard.py | 16 ++--- .../dashboard/graphql/types/feedback.py | 51 ++++++++++---- .../tests/graphql/test_attendance.py | 6 +- .../dashboard/tests/graphql/test_dashboard.py | 8 +-- .../dashboard/tests/graphql/test_feedback.py | 66 +++++++++++++++++-- server/vbv_lernwelt/feedback/utils.py | 15 +++++ server/vbv_lernwelt/feedback/views.py | 7 +- 7 files changed, 133 insertions(+), 36 deletions(-) create mode 100644 server/vbv_lernwelt/feedback/utils.py diff --git a/server/vbv_lernwelt/dashboard/graphql/types/dashboard.py b/server/vbv_lernwelt/dashboard/graphql/types/dashboard.py index 863d7fe7..bfa683f4 100644 --- a/server/vbv_lernwelt/dashboard/graphql/types/dashboard.py +++ b/server/vbv_lernwelt/dashboard/graphql/types/dashboard.py @@ -56,8 +56,8 @@ class CourseDashboardType(graphene.ObjectType): for course_session in course_sessions: course_session_data.append( CourseSessionData( - session_id=course_session.id, - session_title=course_session.title, + session_id=course_session.id, # noqa + session_title=course_session.title, # noqa ) ) generations.add(course_session.generation) @@ -78,9 +78,9 @@ class CourseDashboardType(graphene.ObjectType): for circle in circles: circle_data.append( CircleData( - circle_id=circle.id, - circle_title=circle.title, - experts=[ + circle_id=circle.id, # noqa + circle_title=circle.title, # noqa + experts=[ # noqa f"{su.user.first_name} {su.user.last_name}" for su in circle.expert.all() ], @@ -88,7 +88,7 @@ class CourseDashboardType(graphene.ObjectType): ) return CourseSessionProperties( - sessions=course_session_data, - generations=list(generations), - circles=circle_data, + sessions=course_session_data, # noqa + generations=list(generations), # noqa + circles=circle_data, # noqa ) diff --git a/server/vbv_lernwelt/dashboard/graphql/types/feedback.py b/server/vbv_lernwelt/dashboard/graphql/types/feedback.py index d1704194..1d719d97 100644 --- a/server/vbv_lernwelt/dashboard/graphql/types/feedback.py +++ b/server/vbv_lernwelt/dashboard/graphql/types/feedback.py @@ -5,22 +5,25 @@ import graphene from vbv_lernwelt.core.models import User from vbv_lernwelt.course.models import CourseSession, CourseSessionUser from vbv_lernwelt.feedback.models import FeedbackResponse +from vbv_lernwelt.feedback.utils import feedback_users class FeedbackSummary(graphene.ObjectType): satisfaction_average = graphene.Float() - satisfaction_total = graphene.Int() + satisfaction_max = graphene.Int() total_responses = graphene.Int() -class Record(graphene.ObjectType): +class FeedbackRecord(graphene.ObjectType): course_session_id = graphene.ID() generation = graphene.String() circle_id = graphene.ID() + satisfaction_average = graphene.Float() + satisfaction_max = graphene.Int() class FeedbackResponses(graphene.ObjectType): - records = graphene.List(Record) + records = graphene.List(FeedbackRecord) summary = graphene.Field(FeedbackSummary) @@ -32,22 +35,38 @@ def feedback_responses(course_id: graphene.String(), user: User): course_id=course_id, ) + circle_feedbacks = [] + for course_session in course_sessions: fbs = FeedbackResponse.objects.filter( submitted=True, course_session=course_session, - # Only get feedbacks from members - feedback_user__in=CourseSessionUser.objects.filter( - course_session=course_session, role=CourseSessionUser.Role.MEMBER - ).values_list("user", flat=True), + feedback_user__in=feedback_users(course_session.id), ) - circle_feedbacks = circle_feedback_average(fbs) - return FeedbackResponses() + circle_feedbacks.extend( + circle_feedback_average(fbs, course_session.id, course_session.generation) + ) + + avg = sum([fb.satisfaction_average for fb in circle_feedbacks]) / len( + circle_feedbacks + ) + + return FeedbackResponses( + records=circle_feedbacks, # noqa + summary=FeedbackSummary( # noqa + satisfaction_average=avg, # noqa + satisfaction_max=4, # noqa + total_responses=len(fbs), # noqa + ), + ) -def circle_feedback_average(feedbacks: List[FeedbackResponse]): +def circle_feedback_average( + feedbacks: List[FeedbackResponse], course_session_id, generation: str +): circle_data = {} + records = [] for fb in feedbacks: circle_id = fb.circle.id @@ -60,6 +79,14 @@ def circle_feedback_average(feedbacks: List[FeedbackResponse]): circle_data[circle_id] = {"total": satisfaction, "count": 1} for circle_id, data in circle_data.items(): - circle_data[circle_id]["avg_satisfaction"] = data["total"] / data["count"] + records.append( + FeedbackRecord( + course_session_id=course_session_id, # noqa + generation=generation, # noqa + circle_id=circle_id, # noqa + satisfaction_average=data["total"] / data["count"], # noqa + satisfaction_max=4, # noqa + ) + ) - return circle_data + return records diff --git a/server/vbv_lernwelt/dashboard/tests/graphql/test_attendance.py b/server/vbv_lernwelt/dashboard/tests/graphql/test_attendance.py index e6ddc8ba..5b490898 100644 --- a/server/vbv_lernwelt/dashboard/tests/graphql/test_attendance.py +++ b/server/vbv_lernwelt/dashboard/tests/graphql/test_attendance.py @@ -82,7 +82,7 @@ class DashboardAttendanceTestCase(GraphQLTestCase): attendance_day_presences{{ summary{{ days_completed - participants_present + participants_present }} records{{ course_session_id @@ -91,8 +91,8 @@ class DashboardAttendanceTestCase(GraphQLTestCase): due_date participants_present participants_total - cockpit_url - }} + cockpit_url + }} }} }} }} diff --git a/server/vbv_lernwelt/dashboard/tests/graphql/test_dashboard.py b/server/vbv_lernwelt/dashboard/tests/graphql/test_dashboard.py index 4f305615..afea64f4 100644 --- a/server/vbv_lernwelt/dashboard/tests/graphql/test_dashboard.py +++ b/server/vbv_lernwelt/dashboard/tests/graphql/test_dashboard.py @@ -52,17 +52,17 @@ class DashboardTestCase(GraphQLTestCase): course_dashboard {{ course_id course_title - course_session_properties{{ - sessions{{ + course_session_properties {{ + sessions {{ session_id session_title }} generations - circles{{ + circles {{ circle_id circle_title experts - }} + }} }} }} }} diff --git a/server/vbv_lernwelt/dashboard/tests/graphql/test_feedback.py b/server/vbv_lernwelt/dashboard/tests/graphql/test_feedback.py index ccae60a2..cef2b066 100644 --- a/server/vbv_lernwelt/dashboard/tests/graphql/test_feedback.py +++ b/server/vbv_lernwelt/dashboard/tests/graphql/test_feedback.py @@ -1,7 +1,6 @@ from graphene_django.utils import GraphQLTestCase from vbv_lernwelt.course.models import CourseSessionUser -from vbv_lernwelt.dashboard.graphql.types.feedback import feedback_responses from vbv_lernwelt.dashboard.tests.graphql.utils import ( add_course_session_user, create_circle, @@ -29,6 +28,11 @@ class DashboardFeedbackTestCase(GraphQLTestCase): ) member = create_user("member") + add_course_session_user( + course_session=course_session, + user=member, + role=CourseSessionUser.Role.MEMBER, + ) circle1, _ = create_circle(title="Test Circle 1", course_page=course_page) circle2, _ = create_circle(title="Test Circle 2", course_page=course_page) @@ -38,29 +42,83 @@ class DashboardFeedbackTestCase(GraphQLTestCase): data={"satisfaction": 3}, circle=circle1, course_session=course_session, + submitted=True, ) FeedbackResponse.objects.create( feedback_user=member, data={"satisfaction": 4}, circle=circle1, course_session=course_session, + submitted=True, ) # Create Feedbacks for circle2 FeedbackResponse.objects.create( feedback_user=member, - data={"satisfaction": 5}, + data={"satisfaction": 1}, circle=circle2, course_session=course_session, + submitted=True, ) FeedbackResponse.objects.create( feedback_user=member, data={"satisfaction": 2}, circle=circle2, course_session=course_session, + submitted=True, ) - # Get average satisfaction per circle - result = feedback_responses(course.id, supervisor) + self.client.force_login(supervisor) + + query = f"""query($course_id: ID) {{ + course_dashboard(course_id: $course_id) {{ + course_id + feedback_responses {{ + records {{ + course_session_id + generation + circle_id + satisfaction_average + satisfaction_max + }} + summary {{ + satisfaction_average + satisfaction_max + total_responses + }} + }} + }} + }} + """ + variables = {"course_id": str(course.id)} + + # WHEN + response = self.query(query, variables=variables) # THEN + self.assertResponseNoErrors(response) + + course_dashboard = response.json()["data"]["course_dashboard"] + feedback_responses = course_dashboard[0]["feedback_responses"] + + records = feedback_responses["records"] + self.assertEqual(len(records), 2) + + circle1_record = next( + (r for r in records if r["circle_id"] == str(circle1.id)), None + ) + self.assertEqual(circle1_record["satisfaction_average"], 3.5) + self.assertEqual(circle1_record["course_session_id"], str(course_session.id)) + self.assertEqual(circle1_record["generation"], "2023") + + circle2_record = next( + (r for r in records if r["circle_id"] == str(circle2.id)), None + ) + self.assertEqual(circle2_record["satisfaction_average"], 1.5) + self.assertEqual(circle2_record["course_session_id"], str(course_session.id)) + self.assertEqual(circle2_record["generation"], "2023") + + summary = feedback_responses["summary"] + self.assertEqual(summary["satisfaction_average"], 2.5) + self.assertEqual(summary["satisfaction_max"], 4) + self.assertEqual(summary["total_responses"], 4) diff --git a/server/vbv_lernwelt/feedback/utils.py b/server/vbv_lernwelt/feedback/utils.py new file mode 100644 index 00000000..38ba733e --- /dev/null +++ b/server/vbv_lernwelt/feedback/utils.py @@ -0,0 +1,15 @@ +from django.db.models import Q + +from vbv_lernwelt.core.constants import ADMIN_USER_ID +from vbv_lernwelt.course.models import CourseSessionUser + + +def feedback_users(course_session_id): + """ + Solely accept feedback originating from members of the course session and the illustrious + administrative user, who serves as the repository for feedbacks heretofore submitted anonymously ;-) + """ + return CourseSessionUser.objects.filter( + Q(course_session_id=course_session_id, role=CourseSessionUser.Role.MEMBER) + | Q(user__id=ADMIN_USER_ID) + ).values_list("user", flat=True) diff --git a/server/vbv_lernwelt/feedback/views.py b/server/vbv_lernwelt/feedback/views.py index 85267a9c..ebfd27cf 100644 --- a/server/vbv_lernwelt/feedback/views.py +++ b/server/vbv_lernwelt/feedback/views.py @@ -5,9 +5,9 @@ from rest_framework.decorators import api_view from rest_framework.exceptions import PermissionDenied from rest_framework.response import Response -from vbv_lernwelt.course.models import CourseSessionUser from vbv_lernwelt.course.permissions import is_course_session_expert from vbv_lernwelt.feedback.models import FeedbackResponse +from vbv_lernwelt.feedback.utils import feedback_users logger = structlog.get_logger(__name__) @@ -58,10 +58,7 @@ def get_feedback_for_circle(request, course_session_id, circle_id): course_session__id=course_session_id, submitted=True, circle_id=circle_id, - # filter out experts that might have submitted just for testing - feedback_user__in=CourseSessionUser.objects.filter( - course_session_id=course_session_id, role=CourseSessionUser.Role.MEMBER - ).values_list("user", flat=True), + feedback_user__in=feedback_users(course_session_id), ).order_by("created_at") # I guess this is ok for the üK case From c81a3ab8c78c48cf3c319d9ee16fdce116af2de5 Mon Sep 17 00:00:00 2001 From: Reto Aebersold Date: Fri, 20 Oct 2023 13:37:34 +0200 Subject: [PATCH 08/85] feat: rl dashboard competences --- .../vbv_lernwelt/dashboard/graphql/queries.py | 2 +- .../dashboard/graphql/types/attendance.py | 6 +- .../dashboard/graphql/types/competence.py | 64 ++++++++++++++++++ .../dashboard/graphql/types/dashboard.py | 9 ++- .../dashboard/graphql/types/feedback.py | 2 +- .../tests/graphql/test_competence.py | 66 +++++++++++++++++++ 6 files changed, 143 insertions(+), 6 deletions(-) create mode 100644 server/vbv_lernwelt/dashboard/graphql/types/competence.py create mode 100644 server/vbv_lernwelt/dashboard/tests/graphql/test_competence.py diff --git a/server/vbv_lernwelt/dashboard/graphql/queries.py b/server/vbv_lernwelt/dashboard/graphql/queries.py index 4693c49c..fd508198 100644 --- a/server/vbv_lernwelt/dashboard/graphql/queries.py +++ b/server/vbv_lernwelt/dashboard/graphql/queries.py @@ -22,6 +22,6 @@ class DashboardQuery(graphene.ObjectType): courses = query.distinct() return [ - CourseDashboardType(course_id=course.id, course_title=course.title) + CourseDashboardType(course_id=course.id, course_title=course.title) # noqa for course in courses ] diff --git a/server/vbv_lernwelt/dashboard/graphql/types/attendance.py b/server/vbv_lernwelt/dashboard/graphql/types/attendance.py index 66e38dfa..522b062c 100644 --- a/server/vbv_lernwelt/dashboard/graphql/types/attendance.py +++ b/server/vbv_lernwelt/dashboard/graphql/types/attendance.py @@ -31,12 +31,14 @@ class AttendanceDayPresences(graphene.ObjectType): summary = graphene.Field(AttendanceSummary) -def attendance_day_presences(course_id: graphene.String(), user: User): +def attendance_day_presences( + course_id: graphene.String(), user: User +) -> AttendanceDayPresences: completed = CourseSessionAttendanceCourse.objects.filter( + course_session__course_id=course_id, course_session__coursesessionuser__user=user, course_session__coursesessionuser__role=CourseSessionUser.Role.SESSION_SUPERVISOR, due_date__end__lt=datetime.datetime.now(), - course_session__course_id=course_id, ).order_by("-due_date__end") records = [] diff --git a/server/vbv_lernwelt/dashboard/graphql/types/competence.py b/server/vbv_lernwelt/dashboard/graphql/types/competence.py new file mode 100644 index 00000000..a4b2b1e1 --- /dev/null +++ b/server/vbv_lernwelt/dashboard/graphql/types/competence.py @@ -0,0 +1,64 @@ +import graphene + +from vbv_lernwelt.core.models import User +from vbv_lernwelt.course.models import ( + CourseCompletion, + CourseCompletionStatus, + CourseSessionUser, +) + + +class CompletionSummary(graphene.ObjectType): + success_total = graphene.Int() + fail_total = graphene.Int() + + +class CompetencePerformance(graphene.ObjectType): + course_session_id = graphene.ID() + generation = graphene.String() + circle_id = graphene.ID() + success_count = graphene.Int() + fail_count = graphene.Int() + + +class Competences(graphene.ObjectType): + performances = graphene.List(CompetencePerformance) + summary = graphene.Field(CompletionSummary) + + +def competences(course_id: graphene.String(), user: User) -> Competences: + completions = CourseCompletion.objects.filter( + course_session__course_id=course_id, + course_session__coursesessionuser__user=user, + course_session__coursesessionuser__role=CourseSessionUser.Role.SESSION_SUPERVISOR, + page_type="competence.PerformanceCriteria", + ) + + competence_performances = {} + + for c in completions: + circle = c.page.specific.learning_unit.get_circle() + if circle.id not in competence_performances: + competence_performances[circle.id] = CompetencePerformance( + course_session_id=c.course_session.id, # noqa + generation=c.course_session.generation, # noqa + circle_id=circle.id, # noqa + success_count=0, # noqa + fail_count=0, # noqa + ) + if c.completion_status == CourseCompletionStatus.SUCCESS: + competence_performances[circle.id].success_count += 1 + elif c.completion_status == CourseCompletionStatus.FAIL: + competence_performances[circle.id].fail_count += 1 + + return Competences( + performances=competence_performances.values(), # noqa + summary=CompletionSummary( # noqa + success_total=sum( # noqa + [c.success_count for c in competence_performances.values()] + ), + fail_total=sum( # noqa + [c.fail_count for c in competence_performances.values()] + ), + ), + ) diff --git a/server/vbv_lernwelt/dashboard/graphql/types/dashboard.py b/server/vbv_lernwelt/dashboard/graphql/types/dashboard.py index bfa683f4..3c342c81 100644 --- a/server/vbv_lernwelt/dashboard/graphql/types/dashboard.py +++ b/server/vbv_lernwelt/dashboard/graphql/types/dashboard.py @@ -5,6 +5,7 @@ from vbv_lernwelt.dashboard.graphql.types.attendance import ( attendance_day_presences, AttendanceDayPresences, ) +from vbv_lernwelt.dashboard.graphql.types.competence import competences, Competences from vbv_lernwelt.dashboard.graphql.types.feedback import ( feedback_responses, FeedbackResponses, @@ -35,13 +36,17 @@ class CourseDashboardType(graphene.ObjectType): course_session_properties = graphene.Field(CourseSessionProperties) attendance_day_presences = graphene.Field(AttendanceDayPresences) feedback_responses = graphene.Field(FeedbackResponses) + competences = graphene.Field(Competences) - def resolve_attendance_day_presences(root, info): + def resolve_attendance_day_presences(root, info) -> AttendanceDayPresences: return attendance_day_presences(root.course_id, info.context.user) - def resolve_feedback_responses(root, info): + def resolve_feedback_responses(root, info) -> FeedbackResponses: return feedback_responses(root.course_id, info.context.user) + def resolve_competences(root, info) -> Competences: + return competences(root.course_id, info.context.user) + def resolve_course_session_properties(root, info): course_session_data = [] circle_data = [] diff --git a/server/vbv_lernwelt/dashboard/graphql/types/feedback.py b/server/vbv_lernwelt/dashboard/graphql/types/feedback.py index 1d719d97..231e8803 100644 --- a/server/vbv_lernwelt/dashboard/graphql/types/feedback.py +++ b/server/vbv_lernwelt/dashboard/graphql/types/feedback.py @@ -27,7 +27,7 @@ class FeedbackResponses(graphene.ObjectType): summary = graphene.Field(FeedbackSummary) -def feedback_responses(course_id: graphene.String(), user: User): +def feedback_responses(course_id: graphene.String(), user: User) -> FeedbackResponses: # Get all course sessions for this user in the given course course_sessions = CourseSession.objects.filter( coursesessionuser__user=user, diff --git a/server/vbv_lernwelt/dashboard/tests/graphql/test_competence.py b/server/vbv_lernwelt/dashboard/tests/graphql/test_competence.py new file mode 100644 index 00000000..9708564c --- /dev/null +++ b/server/vbv_lernwelt/dashboard/tests/graphql/test_competence.py @@ -0,0 +1,66 @@ +from graphene_django.utils import GraphQLTestCase + +from vbv_lernwelt.course.models import CourseSessionUser +from vbv_lernwelt.dashboard.tests.graphql.utils import ( + add_course_session_user, + create_circle, + create_course, + create_course_session, + create_user, +) + + +class DashboardCompetenceTestCase(GraphQLTestCase): + GRAPHQL_URL = "/server/graphql/" + + def test_competence(self): + # GIVEN + course, course_page = create_course("Test Course") + course_session = create_course_session(course=course, title="Test Bern 2022 a") + + supervisor = create_user("supervisor") + + add_course_session_user( + course_session=course_session, + user=supervisor, + role=CourseSessionUser.Role.SESSION_SUPERVISOR, + ) + + member = create_user("member") + add_course_session_user( + course_session=course_session, + user=member, + role=CourseSessionUser.Role.MEMBER, + ) + + circle1, _ = create_circle(title="Test Circle 1", course_page=course_page) + circle2, _ = create_circle(title="Test Circle 2", course_page=course_page) + + self.client.force_login(supervisor) + + query = f"""query($course_id: ID) {{ + course_dashboard(course_id: $course_id) {{ + course_id + competences {{ + performances {{ + course_session_id + generation + circle_id + success_count + fail_count + }} + summary {{ + success_total + fail_total + }} + }} + }} + }} + """ + variables = {"course_id": str(course.id)} + + # WHEN + response = self.query(query, variables=variables) + + # THEN + self.assertResponseNoErrors(response) From be789d4fcbc2782430615a2d986a6af6a655c34a Mon Sep 17 00:00:00 2001 From: Reto Aebersold Date: Fri, 20 Oct 2023 15:05:38 +0200 Subject: [PATCH 09/85] feat: rl dashboard competences test --- .../dashboard/graphql/types/competence.py | 24 +++++----- .../tests/graphql/test_competence.py | 48 +++++++++++++++++-- .../dashboard/tests/graphql/utils.py | 43 +++++++++++++++++ 3 files changed, 99 insertions(+), 16 deletions(-) diff --git a/server/vbv_lernwelt/dashboard/graphql/types/competence.py b/server/vbv_lernwelt/dashboard/graphql/types/competence.py index a4b2b1e1..adb3898f 100644 --- a/server/vbv_lernwelt/dashboard/graphql/types/competence.py +++ b/server/vbv_lernwelt/dashboard/graphql/types/competence.py @@ -46,19 +46,19 @@ def competences(course_id: graphene.String(), user: User) -> Competences: success_count=0, # noqa fail_count=0, # noqa ) - if c.completion_status == CourseCompletionStatus.SUCCESS: + if c.completion_status == CourseCompletionStatus.SUCCESS.value: competence_performances[circle.id].success_count += 1 - elif c.completion_status == CourseCompletionStatus.FAIL: + elif c.completion_status == CourseCompletionStatus.FAIL.value: competence_performances[circle.id].fail_count += 1 - return Competences( - performances=competence_performances.values(), # noqa - summary=CompletionSummary( # noqa - success_total=sum( # noqa - [c.success_count for c in competence_performances.values()] - ), - fail_total=sum( # noqa - [c.fail_count for c in competence_performances.values()] - ), + return Competences( + performances=competence_performances.values(), # noqa + summary=CompletionSummary( # noqa + success_total=sum( # noqa + [c.success_count for c in competence_performances.values()] ), - ) + fail_total=sum( # noqa + [c.fail_count for c in competence_performances.values()] + ), + ), + ) diff --git a/server/vbv_lernwelt/dashboard/tests/graphql/test_competence.py b/server/vbv_lernwelt/dashboard/tests/graphql/test_competence.py index 9708564c..5d91c1c1 100644 --- a/server/vbv_lernwelt/dashboard/tests/graphql/test_competence.py +++ b/server/vbv_lernwelt/dashboard/tests/graphql/test_competence.py @@ -1,11 +1,13 @@ from graphene_django.utils import GraphQLTestCase from vbv_lernwelt.course.models import CourseSessionUser +from vbv_lernwelt.course.services import mark_course_completion from vbv_lernwelt.dashboard.tests.graphql.utils import ( add_course_session_user, create_circle, create_course, create_course_session, + create_performance_criteria_page, create_user, ) @@ -26,15 +28,39 @@ class DashboardCompetenceTestCase(GraphQLTestCase): role=CourseSessionUser.Role.SESSION_SUPERVISOR, ) - member = create_user("member") + member_one = create_user("member one") add_course_session_user( course_session=course_session, - user=member, + user=member_one, role=CourseSessionUser.Role.MEMBER, ) - circle1, _ = create_circle(title="Test Circle 1", course_page=course_page) - circle2, _ = create_circle(title="Test Circle 2", course_page=course_page) + member_two = create_user("member two") + add_course_session_user( + course_session=course_session, + user=member_two, + role=CourseSessionUser.Role.MEMBER, + ) + + circle, _ = create_circle(title="Test Circle", course_page=course_page) + + pc = create_performance_criteria_page( + course=course, course_page=course_page, circle=circle + ) + + mark_course_completion( + page=pc, + user=member_one, + course_session=course_session, + completion_status="SUCCESS", + ) + + mark_course_completion( + page=pc, + user=member_two, + course_session=course_session, + completion_status="FAIL", + ) self.client.force_login(supervisor) @@ -64,3 +90,17 @@ class DashboardCompetenceTestCase(GraphQLTestCase): # THEN self.assertResponseNoErrors(response) + + competences = response.json()["data"]["course_dashboard"][0]["competences"] + + performances = competences["performances"] + + self.assertEqual(performances[0]["success_count"], 1) + self.assertEqual(performances[0]["fail_count"], 1) + self.assertEqual(performances[0]["circle_id"], str(circle.id)) + self.assertEqual(performances[0]["course_session_id"], str(course_session.id)) + self.assertEqual(performances[0]["generation"], "2023") + + summary = competences["summary"] + self.assertEqual(summary["success_total"], 1) + self.assertEqual(summary["fail_total"], 1) diff --git a/server/vbv_lernwelt/dashboard/tests/graphql/utils.py b/server/vbv_lernwelt/dashboard/tests/graphql/utils.py index 9e757d78..374d513f 100644 --- a/server/vbv_lernwelt/dashboard/tests/graphql/utils.py +++ b/server/vbv_lernwelt/dashboard/tests/graphql/utils.py @@ -4,10 +4,18 @@ from typing import List, Tuple from django.contrib.auth.hashers import make_password from django.utils import timezone +from vbv_lernwelt.competence.factories import ( + ActionCompetenceFactory, + ActionCompetenceListPageFactory, + CompetenceNaviPageFactory, + PerformanceCriteriaFactory, +) +from vbv_lernwelt.competence.models import PerformanceCriteria from vbv_lernwelt.core.models import User from vbv_lernwelt.course.factories import CoursePageFactory from vbv_lernwelt.course.models import ( Course, + CourseCategory, CoursePage, CourseSession, CourseSessionUser, @@ -20,6 +28,7 @@ from vbv_lernwelt.learnpath.tests.learning_path_factories import ( CircleFactory, LearningContentAttendanceCourseFactory, LearningPathFactory, + LearningUnitFactory, TopicFactory, ) @@ -105,3 +114,37 @@ def create_attendance_course( end=due_date_end, ), ) + + +def create_performance_criteria_page( + course: Course, course_page: CoursePage, circle: Circle +) -> PerformanceCriteria: + competence_navi_page = CompetenceNaviPageFactory( + title="Competence Navi", + parent=course_page, + ) + + competence_profile_page = ActionCompetenceListPageFactory( + title="Action Competence Page", + parent=competence_navi_page, + ) + + action_competence = ActionCompetenceFactory( + parent=competence_profile_page, + competence_id="X1", + title="Action Competence", + items=[("item", "Action Competence Item")], + ) + + cat, _ = CourseCategory.objects.get_or_create( + course=course, title="Course Category" + ) + + lu = LearningUnitFactory(title="Learning Unit", parent=circle, course_category=cat) + + return PerformanceCriteriaFactory( + parent=action_competence, + competence_id="X1.1", + title="Performance Criteria", + learning_unit=lu, + ) From 232270a5069f6590631c7ede1163e0dfa6b760d9 Mon Sep 17 00:00:00 2001 From: Reto Aebersold Date: Fri, 20 Oct 2023 15:10:40 +0200 Subject: [PATCH 10/85] chore: less generic record name --- .../dashboard/graphql/types/attendance.py | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/server/vbv_lernwelt/dashboard/graphql/types/attendance.py b/server/vbv_lernwelt/dashboard/graphql/types/attendance.py index 522b062c..7a37378f 100644 --- a/server/vbv_lernwelt/dashboard/graphql/types/attendance.py +++ b/server/vbv_lernwelt/dashboard/graphql/types/attendance.py @@ -16,7 +16,7 @@ class AttendanceSummary(graphene.ObjectType): participants_present = graphene.Int() -class Record(graphene.ObjectType): +class PresenceRecord(graphene.ObjectType): course_session_id = graphene.ID() generation = graphene.String() circle_id = graphene.ID() @@ -27,7 +27,7 @@ class Record(graphene.ObjectType): class AttendanceDayPresences(graphene.ObjectType): - records = graphene.List(Record) + records = graphene.List(PresenceRecord) summary = graphene.Field(AttendanceSummary) @@ -63,26 +63,26 @@ def attendance_day_presences( ) records.append( - Record( - course_session_id=course_session.id, - generation=course_session.generation, - circle_id=circle.id, - due_date=format_swiss_datetime(attendance_day.due_date.end), - participants_present=participants_present, - participants_total=participants_total, - cockpit_url=url, + PresenceRecord( + course_session_id=course_session.id, # noqa + generation=course_session.generation, # noqa + circle_id=circle.id, # noqa + due_date=format_swiss_datetime(attendance_day.due_date.end), # noqa + participants_present=participants_present, # noqa + participants_total=participants_total, # noqa + cockpit_url=url, # noqa ) ) summary = AttendanceSummary( - days_completed=completed.count(), - participants_present=calculate_avg_participation(records), + days_completed=completed.count(), # noqa + participants_present=calculate_avg_participation(records), # noqa ) - return AttendanceDayPresences(summary=summary, records=records) + return AttendanceDayPresences(summary=summary, records=records) # noqa -def calculate_avg_participation(records: List[Record]) -> float: +def calculate_avg_participation(records: List[PresenceRecord]) -> float: if not records: return 0.0 From d16bb59392179bfd419ef29ab31ede8c189cca1b Mon Sep 17 00:00:00 2001 From: Livio Bieri Date: Thu, 19 Oct 2023 14:56:08 +0200 Subject: [PATCH 11/85] wip: assignment resolving experimental --- .../tests/graphql/test_assignment.py | 280 ++++++++++++++++++ 1 file changed, 280 insertions(+) create mode 100644 server/vbv_lernwelt/dashboard/tests/graphql/test_assignment.py diff --git a/server/vbv_lernwelt/dashboard/tests/graphql/test_assignment.py b/server/vbv_lernwelt/dashboard/tests/graphql/test_assignment.py new file mode 100644 index 00000000..db9ae805 --- /dev/null +++ b/server/vbv_lernwelt/dashboard/tests/graphql/test_assignment.py @@ -0,0 +1,280 @@ +from datetime import datetime +from typing import NamedTuple, List + +from graphene_django.utils import GraphQLTestCase + +from vbv_lernwelt.assignment.models import ( + AssignmentType, + AssignmentCompletion, + Assignment, + AssignmentCompletionStatus, +) +from vbv_lernwelt.assignment.tests.assignment_factories import ( + AssignmentFactory, + AssignmentListPageFactory, +) +from vbv_lernwelt.course.models import CourseSessionUser, CourseSession +from vbv_lernwelt.course_session.models import ( + CourseSessionAssignment, + CourseSessionEdoniqTest, +) +from vbv_lernwelt.dashboard.tests.graphql.utils import ( + add_course_session_user, + create_circle, + create_course, + create_course_session, + create_user, +) +from vbv_lernwelt.learnpath.tests.learning_path_factories import ( + LearningContentAssignmentFactory, + LearningContentEdoniqTestFactory, +) +from vbv_lernwelt.notify.email.email_services import format_swiss_datetime + + +class AssignmentCompletionMetrics(NamedTuple): + passed: int + failed: int + unranked: int + + @property + def ranking_completed(self) -> bool: + """ + Assumption: Completed means all users have been ranked + (passed or failed) -> intermediate states are irrelevant + for the course session supervisor. + """ + return self.unranked == 0 + + @property + def average_passed(self) -> float: + total = self.passed + self.failed + self.unranked + + if total == 0: + return 0 + + return self.passed / total + + +class AssignmentCompletionsStatistics(NamedTuple): + count_completed: int + average_passed: float + + +def calculate_assignment_completions_statistics( + metrics: List[AssignmentCompletionMetrics], +) -> AssignmentCompletionsStatistics: + completed_metrics = [m for m in metrics if m.ranking_completed] + + if not completed_metrics: + return AssignmentCompletionsStatistics(count_completed=0, average_passed=0) + + count_completed = len(completed_metrics) + + average_passed_completed = ( + sum([m.average_passed for m in completed_metrics]) / count_completed + ) + + return AssignmentCompletionsStatistics( + count_completed=count_completed, average_passed=average_passed_completed + ) + + +def get_assignment_completion_metrics( + course_session: CourseSession, assignment: Assignment +) -> AssignmentCompletionMetrics: + course_session_users = CourseSessionUser.objects.filter( + course_session=course_session, + role=CourseSessionUser.Role.MEMBER, + ).values_list("user", flat=True) + + evaluation_results = AssignmentCompletion.objects.filter( + completion_status=AssignmentCompletionStatus.EVALUATION_SUBMITTED, + assignment_user__in=course_session_users, + course_session=course_session, + assignment=assignment, + ).values_list("evaluation_passed", flat=True) + + count_passed = len([passed for passed in evaluation_results if passed]) + count_failed = len(evaluation_results) - count_passed + count_unranked = len(course_session_users) - count_passed - count_failed + + return AssignmentCompletionMetrics( + passed=count_passed, failed=count_failed, unranked=count_unranked + ) + + +class DashboardAssignmentRecord(NamedTuple): + course_session_id: str + circle_id: str + + # type of assignment (translated client-side) + assignment_type_translation_key: str + assignment_title: str + + deadline: datetime + metrics: AssignmentCompletionMetrics + + details_url: str + + @property + def deadline_formatted(self) -> str: + return format_swiss_datetime(self.deadline) + + +def create_dashboard_record( + course_session_assignment: CourseSessionAssignment | CourseSessionEdoniqTest, +) -> DashboardAssignmentRecord: + if isinstance(course_session_assignment, CourseSessionAssignment): + due_date = course_session_assignment.submission_deadline + else: + due_date = course_session_assignment.deadline + + learning_content = course_session_assignment.learning_content + + return DashboardAssignmentRecord( + course_session_id=str(course_session_assignment.course_session.id), + circle_id=learning_content.get_circle().id, + assignment_type_translation_key=due_date.assignment_type_translation_key, + assignment_title=learning_content.content_assignment.title, + metrics=get_assignment_completion_metrics( + course_session=course_session_assignment.course_session, + assignment=learning_content.content_assignment, + ), + details_url=due_date.url_expert, + deadline=due_date.start, + ) + + +def trybetter(course_session): + dashboard_records: List[DashboardAssignmentRecord] = [] + + for course_session_assignment in CourseSessionAssignment.objects.filter( + course_session=course_session, + learning_content__assignment_type__in=[ + AssignmentType.CASEWORK.value, + AssignmentType.PREP_ASSIGNMENT.value, + ], + ): + dashboard_record = create_dashboard_record(course_session_assignment) + dashboard_records.append(dashboard_record) + + for course_session_edoniq_test in CourseSessionEdoniqTest.objects.filter( + course_session=course_session + ): + dashboard_record = create_dashboard_record(course_session_edoniq_test) + dashboard_records.append(dashboard_record) + + return sorted(dashboard_records, key=lambda r: r.deadline) + + +class AssignmentTestCase(GraphQLTestCase): + GRAPHQL_URL = "/server/graphql/" + + def test_assignment_something_not_yet_sure_what_exactly_who_knows(self): + # GIVEN + course, course_page = create_course("Test Course") + course_session = create_course_session(course=course, title="Test Bern 2022 a") + circle, _ = create_circle(title="Test Circle", course_page=course_page) + + supervisor = create_user("supervisor") + add_course_session_user( + course_session=course_session, + user=supervisor, + role=CourseSessionUser.Role.SESSION_SUPERVISOR, + ) + + m1 = create_user("member_1") + add_course_session_user( + course_session=course_session, + user=m1, + role=CourseSessionUser.Role.MEMBER, + ) + + m2 = create_user("member_2") + add_course_session_user( + course_session=course_session, + user=m2, + role=CourseSessionUser.Role.MEMBER, + ) + + m3 = create_user("member_3") + add_course_session_user( + course_session=course_session, + user=m3, + role=CourseSessionUser.Role.MEMBER, + ) + + e1 = create_user("expert_1") + add_course_session_user( + course_session=course_session, + user=e1, + role=CourseSessionUser.Role.EXPERT, + ) + + assignment = AssignmentFactory( + parent=AssignmentListPageFactory( + parent=course.coursepage, + ), + assignment_type=AssignmentType.CASEWORK.name, + title="Test Assignment", + effort_required="However long it takes", + intro_text="Assignment Intro Text", + performance_objectives=[], + ) + + edoniq_test = AssignmentFactory( + parent=AssignmentListPageFactory( + parent=course.coursepage, + ), + assignment_type=AssignmentType.EDONIQ_TEST.name, + title="Edoniq Test Assignment", + effort_required="However long it takes", + intro_text="Edoniq Test Assigment Intro Text", + performance_objectives=[], + ) + + AssignmentCompletion.objects.create( + assignment_user=m1, + assignment=edoniq_test, + evaluation_passed=True, + course_session=course_session, + completion_data={}, + ) + + AssignmentCompletion.objects.create( + assignment_user=m1, + assignment=assignment, + evaluation_passed=True, + course_session=course_session, + completion_data={}, + ) + + learning_content_assignment = LearningContentAssignmentFactory( + title="Learning Content Assignment Title", + parent=circle, + content_assignment=assignment, + ) + + learning_content_edoniq_test = LearningContentEdoniqTestFactory( + title="Learning Content Edoniq Test Title", + parent=circle, + content_assignment=edoniq_test, + ) + + CourseSessionAssignment.objects.create( + course_session=course_session, learning_content=learning_content_assignment + ) + + CourseSessionEdoniqTest.objects.create( + course_session=course_session, learning_content=learning_content_edoniq_test + ) + + # --------------------------------------------------------------------- + # --------------------------------------------------------------------- + # --------------------------------------------------------------------- + # --------------------------------------------------------------------- + + trybetter(course_session) + + self.client.force_login(supervisor) From dc706e7ece455b72eb1398102bdfe8e3d468f0db Mon Sep 17 00:00:00 2001 From: Livio Bieri Date: Fri, 20 Oct 2023 19:40:00 +0200 Subject: [PATCH 12/85] feat: adds graphql assignment dashboard --- .../dashboard/graphql/types/assignment.py | 156 +++++ .../dashboard/graphql/types/attendance.py | 4 +- .../dashboard/graphql/types/dashboard.py | 8 + .../tests/graphql/test_assignment.py | 534 ++++++++++-------- .../dashboard/tests/graphql/utils.py | 111 +++- 5 files changed, 585 insertions(+), 228 deletions(-) create mode 100644 server/vbv_lernwelt/dashboard/graphql/types/assignment.py diff --git a/server/vbv_lernwelt/dashboard/graphql/types/assignment.py b/server/vbv_lernwelt/dashboard/graphql/types/assignment.py new file mode 100644 index 00000000..07ad0ead --- /dev/null +++ b/server/vbv_lernwelt/dashboard/graphql/types/assignment.py @@ -0,0 +1,156 @@ +import math +from typing import List + +import graphene + +import vbv_lernwelt.assignment.models +from vbv_lernwelt.assignment.models import ( + AssignmentCompletion, + AssignmentCompletionStatus, + AssignmentType, +) +from vbv_lernwelt.course.models import CourseSession, CourseSessionUser +from vbv_lernwelt.course_session.models import ( + CourseSessionAssignment, + CourseSessionEdoniqTest, +) + + +class AssignmentCompletionMetrics(graphene.ObjectType): + passed_count = graphene.Int(required=True) + failed_count = graphene.Int(required=True) + unranked_count = graphene.Int(required=True) + ranking_completed = graphene.Boolean(required=True) + average_passed = graphene.Float(required=True) + + +class AssignmentRecord(graphene.ObjectType): + course_session_id = graphene.ID(required=True) + course_session_assignment_id = graphene.ID(required=True) + circle_id = graphene.ID(required=True) + generation = graphene.String(required=True) + assignment_type_translation_key = graphene.String(required=True) + assignment_title = graphene.String(required=True) + deadline = graphene.DateTime(required=True) + metrics = graphene.Field(AssignmentCompletionMetrics, required=True) + details_url = graphene.String(required=True) + + +class AssignmentSummary(graphene.ObjectType): + completed_count = graphene.Int(required=True) + average_passed = graphene.Float(required=True) + + +class Assignments(graphene.ObjectType): + records = graphene.List(AssignmentRecord, required=True) + summary = graphene.Field(AssignmentSummary, required=True) + + +def create_assignment_summary(metrics) -> AssignmentSummary: + completed_metrics = [m for m in metrics if m.ranking_completed] + + if not completed_metrics: + return AssignmentSummary(completed_count=0, average_passed=0) # noqa + + completed_count = len(completed_metrics) + + average_passed_completed = ( + sum([m.average_passed for m in completed_metrics]) / completed_count + ) + + return AssignmentSummary( + completed_count=completed_count, average_passed=average_passed_completed # noqa + ) + + +def get_assignment_completion_metrics( + course_session: CourseSession, assignment: vbv_lernwelt.assignment.models.Assignment +) -> AssignmentCompletionMetrics: + course_session_users = CourseSessionUser.objects.filter( + course_session=course_session, + role=CourseSessionUser.Role.MEMBER, + ).values_list("user", flat=True) + + evaluation_results = AssignmentCompletion.objects.filter( + completion_status=AssignmentCompletionStatus.EVALUATION_SUBMITTED.value, + assignment_user__in=course_session_users, + course_session=course_session, + assignment=assignment, + ).values_list("evaluation_passed", flat=True) + + passed_count = len([passed for passed in evaluation_results if passed]) + failed_count = len(evaluation_results) - passed_count + + participants_count = len(course_session_users) + unranked_count = participants_count - passed_count - failed_count + + if participants_count == 0: + average_passed = 0 + else: + average_passed = math.ceil(passed_count / participants_count * 100) + + return AssignmentCompletionMetrics( + passed_count=passed_count, # noqa + failed_count=failed_count, # noqa + unranked_count=unranked_count, # noqa + ranking_completed=unranked_count == 0, # noqa + average_passed=average_passed, # noqa + ) + + +def create_record( + course_session_assignment: CourseSessionAssignment | CourseSessionEdoniqTest, +) -> AssignmentRecord: + if isinstance(course_session_assignment, CourseSessionAssignment): + due_date = course_session_assignment.submission_deadline + else: + due_date = course_session_assignment.deadline + + learning_content = course_session_assignment.learning_content + + return AssignmentRecord( + course_session_id=str(course_session_assignment.course_session.id), # noqa + circle_id=learning_content.get_circle().id, # noqa + course_session_assignment_id=str(course_session_assignment.id), # noqa + generation=course_session_assignment.course_session.generation, # noqa + assignment_type_translation_key=due_date.assignment_type_translation_key, # noqa + assignment_title=learning_content.content_assignment.title, # noqa + metrics=get_assignment_completion_metrics( # noqa + course_session=course_session_assignment.course_session, # noqa + assignment=learning_content.content_assignment, # noqa + ), + details_url=due_date.url_expert, # noqa + deadline=due_date.start, # noqa + ) + + +def assignments(course_id, user) -> Assignments: + course_sessions = CourseSession.objects.filter( + coursesessionuser__user=user, + coursesessionuser__role=CourseSessionUser.Role.SESSION_SUPERVISOR, + course_id=course_id, + ) + + records: List[AssignmentRecord] = [] + + for course_session in course_sessions: + for csa in CourseSessionAssignment.objects.filter( + course_session=course_session, + learning_content__assignment_type__in=[ + AssignmentType.CASEWORK.value, + AssignmentType.PREP_ASSIGNMENT.value, + ], + ): + record = create_record(csa) + records.append(record) + + for cset in CourseSessionEdoniqTest.objects.filter( + course_session=course_session + ): + record = create_record(cset) + records.append(record) + + return Assignments( + records=sorted(records, key=lambda r: r.deadline), # noqa + summary=create_assignment_summary([r.metrics for r in records]), # noqa + ) diff --git a/server/vbv_lernwelt/dashboard/graphql/types/attendance.py b/server/vbv_lernwelt/dashboard/graphql/types/attendance.py index 7a37378f..3db5fa69 100644 --- a/server/vbv_lernwelt/dashboard/graphql/types/attendance.py +++ b/server/vbv_lernwelt/dashboard/graphql/types/attendance.py @@ -1,8 +1,8 @@ -import datetime import math from typing import List import graphene +from django.utils import timezone from vbv_lernwelt.core.models import User from vbv_lernwelt.course.models import CourseSessionUser @@ -38,7 +38,7 @@ def attendance_day_presences( course_session__course_id=course_id, course_session__coursesessionuser__user=user, course_session__coursesessionuser__role=CourseSessionUser.Role.SESSION_SUPERVISOR, - due_date__end__lt=datetime.datetime.now(), + due_date__end__lt=timezone.now(), ).order_by("-due_date__end") records = [] diff --git a/server/vbv_lernwelt/dashboard/graphql/types/dashboard.py b/server/vbv_lernwelt/dashboard/graphql/types/dashboard.py index 3c342c81..2d1435c9 100644 --- a/server/vbv_lernwelt/dashboard/graphql/types/dashboard.py +++ b/server/vbv_lernwelt/dashboard/graphql/types/dashboard.py @@ -1,6 +1,10 @@ import graphene from vbv_lernwelt.course.models import CourseSession, CourseSessionUser +from vbv_lernwelt.dashboard.graphql.types.assignment import ( + Assignments, + assignments, +) from vbv_lernwelt.dashboard.graphql.types.attendance import ( attendance_day_presences, AttendanceDayPresences, @@ -36,6 +40,7 @@ class CourseDashboardType(graphene.ObjectType): course_session_properties = graphene.Field(CourseSessionProperties) attendance_day_presences = graphene.Field(AttendanceDayPresences) feedback_responses = graphene.Field(FeedbackResponses) + assignments = graphene.Field(Assignments) competences = graphene.Field(Competences) def resolve_attendance_day_presences(root, info) -> AttendanceDayPresences: @@ -47,6 +52,9 @@ class CourseDashboardType(graphene.ObjectType): def resolve_competences(root, info) -> Competences: return competences(root.course_id, info.context.user) + def resolve_assignments(root, info): + return assignments(root.course_id, info.context.user) + def resolve_course_session_properties(root, info): course_session_data = [] circle_data = [] diff --git a/server/vbv_lernwelt/dashboard/tests/graphql/test_assignment.py b/server/vbv_lernwelt/dashboard/tests/graphql/test_assignment.py index db9ae805..2df40247 100644 --- a/server/vbv_lernwelt/dashboard/tests/graphql/test_assignment.py +++ b/server/vbv_lernwelt/dashboard/tests/graphql/test_assignment.py @@ -1,17 +1,11 @@ from datetime import datetime -from typing import NamedTuple, List +from typing import Tuple from graphene_django.utils import GraphQLTestCase from vbv_lernwelt.assignment.models import ( AssignmentType, - AssignmentCompletion, Assignment, - AssignmentCompletionStatus, -) -from vbv_lernwelt.assignment.tests.assignment_factories import ( - AssignmentFactory, - AssignmentListPageFactory, ) from vbv_lernwelt.course.models import CourseSessionUser, CourseSession from vbv_lernwelt.course_session.models import ( @@ -24,257 +18,349 @@ from vbv_lernwelt.dashboard.tests.graphql.utils import ( create_course, create_course_session, create_user, + create_assignment, + create_assignment_completion, + create_assignment_learning_content, + create_course_session_edoniq_test, + create_course_session_assignment, ) -from vbv_lernwelt.learnpath.tests.learning_path_factories import ( - LearningContentAssignmentFactory, - LearningContentEdoniqTestFactory, -) -from vbv_lernwelt.notify.email.email_services import format_swiss_datetime - - -class AssignmentCompletionMetrics(NamedTuple): - passed: int - failed: int - unranked: int - - @property - def ranking_completed(self) -> bool: - """ - Assumption: Completed means all users have been ranked - (passed or failed) -> intermediate states are irrelevant - for the course session supervisor. - """ - return self.unranked == 0 - - @property - def average_passed(self) -> float: - total = self.passed + self.failed + self.unranked - - if total == 0: - return 0 - - return self.passed / total - - -class AssignmentCompletionsStatistics(NamedTuple): - count_completed: int - average_passed: float - - -def calculate_assignment_completions_statistics( - metrics: List[AssignmentCompletionMetrics], -) -> AssignmentCompletionsStatistics: - completed_metrics = [m for m in metrics if m.ranking_completed] - - if not completed_metrics: - return AssignmentCompletionsStatistics(count_completed=0, average_passed=0) - - count_completed = len(completed_metrics) - - average_passed_completed = ( - sum([m.average_passed for m in completed_metrics]) / count_completed - ) - - return AssignmentCompletionsStatistics( - count_completed=count_completed, average_passed=average_passed_completed - ) - - -def get_assignment_completion_metrics( - course_session: CourseSession, assignment: Assignment -) -> AssignmentCompletionMetrics: - course_session_users = CourseSessionUser.objects.filter( - course_session=course_session, - role=CourseSessionUser.Role.MEMBER, - ).values_list("user", flat=True) - - evaluation_results = AssignmentCompletion.objects.filter( - completion_status=AssignmentCompletionStatus.EVALUATION_SUBMITTED, - assignment_user__in=course_session_users, - course_session=course_session, - assignment=assignment, - ).values_list("evaluation_passed", flat=True) - - count_passed = len([passed for passed in evaluation_results if passed]) - count_failed = len(evaluation_results) - count_passed - count_unranked = len(course_session_users) - count_passed - count_failed - - return AssignmentCompletionMetrics( - passed=count_passed, failed=count_failed, unranked=count_unranked - ) - - -class DashboardAssignmentRecord(NamedTuple): - course_session_id: str - circle_id: str - - # type of assignment (translated client-side) - assignment_type_translation_key: str - assignment_title: str - - deadline: datetime - metrics: AssignmentCompletionMetrics - - details_url: str - - @property - def deadline_formatted(self) -> str: - return format_swiss_datetime(self.deadline) - - -def create_dashboard_record( - course_session_assignment: CourseSessionAssignment | CourseSessionEdoniqTest, -) -> DashboardAssignmentRecord: - if isinstance(course_session_assignment, CourseSessionAssignment): - due_date = course_session_assignment.submission_deadline - else: - due_date = course_session_assignment.deadline - - learning_content = course_session_assignment.learning_content - - return DashboardAssignmentRecord( - course_session_id=str(course_session_assignment.course_session.id), - circle_id=learning_content.get_circle().id, - assignment_type_translation_key=due_date.assignment_type_translation_key, - assignment_title=learning_content.content_assignment.title, - metrics=get_assignment_completion_metrics( - course_session=course_session_assignment.course_session, - assignment=learning_content.content_assignment, - ), - details_url=due_date.url_expert, - deadline=due_date.start, - ) - - -def trybetter(course_session): - dashboard_records: List[DashboardAssignmentRecord] = [] - - for course_session_assignment in CourseSessionAssignment.objects.filter( - course_session=course_session, - learning_content__assignment_type__in=[ - AssignmentType.CASEWORK.value, - AssignmentType.PREP_ASSIGNMENT.value, - ], - ): - dashboard_record = create_dashboard_record(course_session_assignment) - dashboard_records.append(dashboard_record) - - for course_session_edoniq_test in CourseSessionEdoniqTest.objects.filter( - course_session=course_session - ): - dashboard_record = create_dashboard_record(course_session_edoniq_test) - dashboard_records.append(dashboard_record) - - return sorted(dashboard_records, key=lambda r: r.deadline) +from vbv_lernwelt.learnpath.models import Circle class AssignmentTestCase(GraphQLTestCase): GRAPHQL_URL = "/server/graphql/" + GRAPHQL_QUERY = f"""query($course_id: ID) {{ + course_dashboard(course_id: $course_id) {{ + assignments{{ + summary{{ + completed_count + average_passed + }} + records{{ + course_session_id + course_session_assignment_id + circle_id + generation + assignment_title + assignment_type_translation_key + details_url + deadline + metrics {{ + passed_count + failed_count + unranked_count + ranking_completed + average_passed + }} + }} + }} + }} + }}""" - def test_assignment_something_not_yet_sure_what_exactly_who_knows(self): - # GIVEN - course, course_page = create_course("Test Course") - course_session = create_course_session(course=course, title="Test Bern 2022 a") - circle, _ = create_circle(title="Test Circle", course_page=course_page) + def setUp(self): + self.course, self.course_page = create_course("Test Course") + self.course_session = create_course_session(course=self.course, title=":)") + self.circle, _ = create_circle(title="Circle", course_page=self.course_page) - supervisor = create_user("supervisor") + self.supervisor = create_user("supervisor") add_course_session_user( - course_session=course_session, - user=supervisor, + course_session=self.course_session, + user=self.supervisor, role=CourseSessionUser.Role.SESSION_SUPERVISOR, ) - m1 = create_user("member_1") + self.m1 = create_user("member_1") add_course_session_user( - course_session=course_session, - user=m1, + course_session=self.course_session, + user=self.m1, role=CourseSessionUser.Role.MEMBER, ) - m2 = create_user("member_2") + self.m2 = create_user("member_2") add_course_session_user( - course_session=course_session, - user=m2, + course_session=self.course_session, + user=self.m2, role=CourseSessionUser.Role.MEMBER, ) - m3 = create_user("member_3") + self.m3 = create_user("member_3") add_course_session_user( - course_session=course_session, - user=m3, + course_session=self.course_session, + user=self.m3, role=CourseSessionUser.Role.MEMBER, ) - e1 = create_user("expert_1") + self.e1 = create_user("expert_1") add_course_session_user( - course_session=course_session, - user=e1, + course_session=self.course_session, + user=self.e1, role=CourseSessionUser.Role.EXPERT, ) - assignment = AssignmentFactory( - parent=AssignmentListPageFactory( - parent=course.coursepage, - ), - assignment_type=AssignmentType.CASEWORK.name, - title="Test Assignment", - effort_required="However long it takes", - intro_text="Assignment Intro Text", - performance_objectives=[], + self.client.force_login(self.supervisor) + + def test_dashboard_contains_casework(self): + self._test_assignment_type_dashboard_details( + assignment_type=AssignmentType.CASEWORK ) - edoniq_test = AssignmentFactory( - parent=AssignmentListPageFactory( - parent=course.coursepage, - ), - assignment_type=AssignmentType.EDONIQ_TEST.name, - title="Edoniq Test Assignment", - effort_required="However long it takes", - intro_text="Edoniq Test Assigment Intro Text", - performance_objectives=[], + def test_dashboard_contains_prep_assignments(self): + self._test_assignment_type_dashboard_details( + assignment_type=AssignmentType.PREP_ASSIGNMENT ) - AssignmentCompletion.objects.create( - assignment_user=m1, - assignment=edoniq_test, - evaluation_passed=True, - course_session=course_session, - completion_data={}, + def test_dashboard_contains_edoniq_tests(self): + self._test_assignment_type_dashboard_details( + assignment_type=AssignmentType.EDONIQ_TEST ) - AssignmentCompletion.objects.create( - assignment_user=m1, + def test_dashboard_not_contains_unsupported_types(self): + """ + Since everything is mixed in the same table, we need to make sure + that the dashboard only contains the supported types does not + get confused by the unsupported ones. + """ + + irrelevant_types_for_dashboard = set(AssignmentType) - { + AssignmentType.CASEWORK, + AssignmentType.PREP_ASSIGNMENT, + AssignmentType.EDONIQ_TEST, + } + + for assignment_type in irrelevant_types_for_dashboard: + self._test_assignment_type_not_in_dashboard(assignment_type=assignment_type) + + def _test_assignment_type_dashboard_details(self, assignment_type: AssignmentType): + # GIVEN + assignment, csa = mix_assignment_cocktail( + assignment_type=assignment_type, + deadline_at=datetime(2000, 4, 1), + course_session=self.course_session, + circle=self.circle, + ) + + create_assignment_completion( + user=self.m1, assignment=assignment, - evaluation_passed=True, + course_session=self.course_session, + ) + + # WHEN + response = self.query( + self.GRAPHQL_QUERY, variables={"course_id": str(self.course.id)} + ) + + # THEN + self.assertResponseNoErrors(response) + dashboard = response.json()["data"]["course_dashboard"] + + records = dashboard[0]["assignments"]["records"] + self.assertEqual(len(records), 1) + + record = records[0] + + if isinstance(csa, CourseSessionAssignment): + due_date = csa.submission_deadline + else: + due_date = csa.deadline + + self.assertEqual(record["course_session_id"], str(self.course_session.id)) + self.assertEqual(record["course_session_assignment_id"], str(csa.id)) + self.assertEqual(record["generation"], str(self.course_session.generation)) + self.assertEqual(record["circle_id"], str(self.circle.id)) + self.assertEqual(record["details_url"], due_date.url_expert) + self.assertEqual(datetime.fromisoformat(record["deadline"]), due_date.start) + + self.assertEqual( + record["assignment_title"], + csa.learning_content.content_assignment.title, + ) + self.assertEqual( + record["assignment_type_translation_key"], + due_date.assignment_type_translation_key, + ) + + def _test_assignment_type_not_in_dashboard(self, assignment_type: AssignmentType): + _, csa = mix_assignment_cocktail( + assignment_type=assignment_type, + course_session=self.course_session, + circle=self.circle, + ) + + # WHEN + response = self.query( + self.GRAPHQL_QUERY, variables={"course_id": str(self.course.id)} + ) + + # THEN + self.assertResponseNoErrors(response) + dashboard = response.json()["data"]["course_dashboard"] + + records = dashboard[0]["assignments"]["records"] + self.assertEqual(len(records), 0) + + def test_metrics_summary(self): + # GIVEN + assignment_1, _ = mix_assignment_cocktail( + deadline_at=datetime(1990, 4, 1), + assignment_type=AssignmentType.CASEWORK, + course_session=self.course_session, + circle=self.circle, + ) + + assignment_2, _ = mix_assignment_cocktail( + deadline_at=datetime(2000, 4, 1), + assignment_type=AssignmentType.EDONIQ_TEST, + course_session=self.course_session, + circle=self.circle, + ) + + assignment_3, _ = mix_assignment_cocktail( + deadline_at=datetime(2010, 4, 1), + assignment_type=AssignmentType.PREP_ASSIGNMENT, + course_session=self.course_session, + circle=self.circle, + ) + + # no completions for this assignment yet + assignment_4, _ = mix_assignment_cocktail( + deadline_at=datetime(2020, 4, 1), + assignment_type=AssignmentType.EDONIQ_TEST, + course_session=self.course_session, + circle=self.circle, + ) + + # assignment 1 + assigment_1_results = [ + (self.m1, True), # passed + (self.m2, False), # failed + (self.m3, None), # unranked + ] + + for user, has_passed in assigment_1_results: + if has_passed is None: + continue + create_assignment_completion( + user=user, + assignment=assignment_1, + course_session=self.course_session, + has_passed=has_passed, + ) + + # assignment 2 + assignment_2_results = [ + (self.m1, True), # passed + (self.m2, True), # passed + (self.m3, False), # failed + ] + + for user, has_passed in assignment_2_results: + create_assignment_completion( + user=user, + assignment=assignment_2, + course_session=self.course_session, + has_passed=has_passed, + ) + + # assignment 3 + assignment_3_results = [ + (self.m1, True), # passed + (self.m2, True), # passed + (self.m3, True), # passed + ] + for user, has_passed in assignment_3_results: + create_assignment_completion( + user=user, + assignment=assignment_3, + course_session=self.course_session, + has_passed=has_passed, + ) + + # WHEN + response = self.query( + self.GRAPHQL_QUERY, variables={"course_id": str(self.course.id)} + ) + + # THEN + self.assertResponseNoErrors(response) + dashboard = response.json()["data"]["course_dashboard"] + + # 1 -> incomplete (not counted for average) + # 2 -> complete 66% passed ... + # 3 -> complete 100% passed --> 83.5% + # 4 -> incomplete (not counted for average) + summary = dashboard[0]["assignments"]["summary"] + self.assertEqual(summary["completed_count"], 2) + self.assertEqual(summary["average_passed"], 83.5) + + records = dashboard[0]["assignments"]["records"] + self.assertEqual(len(records), 4) + + # 1 -> assigment_1_results (oldest) + assignment_1_metrics = records[0]["metrics"] + self.assertEqual(assignment_1_metrics["passed_count"], 1) + self.assertEqual(assignment_1_metrics["failed_count"], 1) + self.assertEqual(assignment_1_metrics["unranked_count"], 1) + self.assertEqual(assignment_1_metrics["ranking_completed"], False) + self.assertEqual(assignment_1_metrics["average_passed"], 34) + + # 2 -> assignment_2_results + assignment_2_metrics = records[1]["metrics"] + self.assertEqual(assignment_2_metrics["passed_count"], 2) + self.assertEqual(assignment_2_metrics["failed_count"], 1) + self.assertEqual(assignment_2_metrics["unranked_count"], 0) + self.assertEqual(assignment_2_metrics["ranking_completed"], True) + self.assertEqual(assignment_2_metrics["average_passed"], 67) + + # 3 -> assignment_3_results + assignment_3_metrics = records[2]["metrics"] + self.assertEqual(assignment_3_metrics["passed_count"], 3) + self.assertEqual(assignment_3_metrics["failed_count"], 0) + self.assertEqual(assignment_3_metrics["unranked_count"], 0) + self.assertEqual(assignment_3_metrics["ranking_completed"], True) + self.assertEqual(assignment_3_metrics["average_passed"], 100) + + # 4 -> no completions (newest) + assignment_4_metrics = records[3]["metrics"] + self.assertEqual(assignment_4_metrics["passed_count"], 0) + self.assertEqual(assignment_4_metrics["failed_count"], 0) + self.assertEqual(assignment_4_metrics["unranked_count"], 3) + self.assertEqual(assignment_4_metrics["ranking_completed"], False) + self.assertEqual(assignment_4_metrics["average_passed"], 0) + + +def mix_assignment_cocktail( + assignment_type: AssignmentType, + course_session: CourseSession, + circle: Circle, + deadline_at: datetime | None = None, +) -> Tuple[Assignment, CourseSessionAssignment | CourseSessionEdoniqTest]: + """ + Little test helper to create a course session assignment or edoniq test based + on the given assignment type. + """ + + assignment = create_assignment( + course=course_session.course, assignment_type=assignment_type + ) + + if assignment_type == AssignmentType.EDONIQ_TEST: + cset = create_course_session_edoniq_test( + deadline_at=deadline_at, course_session=course_session, - completion_data={}, + learning_content_edoniq_test=create_assignment_learning_content( + circle=circle, + assignment=assignment, + ), ) - - learning_content_assignment = LearningContentAssignmentFactory( - title="Learning Content Assignment Title", - parent=circle, - content_assignment=assignment, + return assignment, cset + else: + csa = create_course_session_assignment( + deadline_at=deadline_at, + course_session=course_session, + learning_content_assignment=create_assignment_learning_content( + circle=circle, + assignment=assignment, + ), ) - - learning_content_edoniq_test = LearningContentEdoniqTestFactory( - title="Learning Content Edoniq Test Title", - parent=circle, - content_assignment=edoniq_test, - ) - - CourseSessionAssignment.objects.create( - course_session=course_session, learning_content=learning_content_assignment - ) - - CourseSessionEdoniqTest.objects.create( - course_session=course_session, learning_content=learning_content_edoniq_test - ) - - # --------------------------------------------------------------------- - # --------------------------------------------------------------------- - # --------------------------------------------------------------------- - # --------------------------------------------------------------------- - - trybetter(course_session) - - self.client.force_login(supervisor) + return assignment, csa diff --git a/server/vbv_lernwelt/dashboard/tests/graphql/utils.py b/server/vbv_lernwelt/dashboard/tests/graphql/utils.py index 374d513f..e67621e5 100644 --- a/server/vbv_lernwelt/dashboard/tests/graphql/utils.py +++ b/server/vbv_lernwelt/dashboard/tests/graphql/utils.py @@ -4,6 +4,16 @@ from typing import List, Tuple from django.contrib.auth.hashers import make_password from django.utils import timezone +from vbv_lernwelt.assignment.models import ( + AssignmentType, + AssignmentCompletion, + Assignment, + AssignmentCompletionStatus, +) +from vbv_lernwelt.assignment.tests.assignment_factories import ( + AssignmentFactory, + AssignmentListPageFactory, +) from vbv_lernwelt.competence.factories import ( ActionCompetenceFactory, ActionCompetenceListPageFactory, @@ -21,15 +31,26 @@ from vbv_lernwelt.course.models import ( CourseSessionUser, ) from vbv_lernwelt.course.utils import get_wagtail_default_site -from vbv_lernwelt.course_session.models import CourseSessionAttendanceCourse +from vbv_lernwelt.course_session.models import ( + CourseSessionAttendanceCourse, + CourseSessionAssignment, + CourseSessionEdoniqTest, +) from vbv_lernwelt.duedate.models import DueDate -from vbv_lernwelt.learnpath.models import Circle, LearningPath +from vbv_lernwelt.learnpath.models import ( + Circle, + LearningPath, + LearningContentAssignment, + LearningContentEdoniqTest, +) from vbv_lernwelt.learnpath.tests.learning_path_factories import ( CircleFactory, LearningContentAttendanceCourseFactory, LearningPathFactory, LearningUnitFactory, TopicFactory, + LearningContentAssignmentFactory, + LearningContentEdoniqTestFactory, ) @@ -116,6 +137,92 @@ def create_attendance_course( ) +def create_assignment( + course: Course, + assignment_type: AssignmentType, +) -> Assignment: + return AssignmentFactory( + parent=AssignmentListPageFactory( + parent=course.coursepage, + ), + assignment_type=assignment_type.name, + title=f"Dummy Assignment ({assignment_type.name})", + effort_required=":)", + intro_text=":)", + performance_objectives=[], + ) + + +def create_assignment_completion( + user: User, + assignment: Assignment, + course_session: CourseSession, + has_passed: bool | None = None, +) -> AssignmentCompletion: + return AssignmentCompletion.objects.create( + completion_status=AssignmentCompletionStatus.EVALUATION_SUBMITTED.value, + assignment_user=user, + assignment=assignment, + evaluation_passed=has_passed, + course_session=course_session, + completion_data={}, + ) + + +def create_assignment_learning_content( + circle: Circle, + assignment: Assignment, +) -> LearningContentAssignment | LearningContentEdoniqTest: + if AssignmentType(assignment.assignment_type) == AssignmentType.EDONIQ_TEST: + return LearningContentEdoniqTestFactory( + title="Learning Content (EDONIQ_TEST)", + parent=circle, + content_assignment=assignment, + ) + + return LearningContentAssignmentFactory( + title=f"Learning Content ({assignment.assignment_type})", + parent=circle, + content_assignment=assignment, + ) + + +def create_course_session_assignment( + course_session: CourseSession, + learning_content_assignment: LearningContentAssignment, + deadline_at: datetime | None = None, +) -> CourseSessionAssignment: + cas = CourseSessionAssignment.objects.create( + course_session=course_session, + learning_content=learning_content_assignment, + ) + + if deadline_at: + # the save on the course_session_assignment already sets a lot + # of due date fields, so it's easier to just overwrite the this + cas.submission_deadline.start = timezone.make_aware(deadline_at) + cas.submission_deadline.save() + + return cas + + +def create_course_session_edoniq_test( + course_session: CourseSession, + learning_content_edoniq_test: LearningContentEdoniqTest, + deadline_at: datetime, +) -> CourseSessionEdoniqTest: + cset = CourseSessionEdoniqTest.objects.create( + course_session=course_session, + learning_content=learning_content_edoniq_test, + ) + + # same as above (see create_course_session_assignment) + cset.deadline.start = timezone.make_aware(deadline_at) + cset.deadline.save() + + return cset + + def create_performance_criteria_page( course: Course, course_page: CoursePage, circle: Circle ) -> PerformanceCriteria: From 48677974d2680920f8ad44b1f0e7c7011a0ebcc8 Mon Sep 17 00:00:00 2001 From: Livio Bieri Date: Fri, 20 Oct 2023 19:41:36 +0200 Subject: [PATCH 13/85] fix: format --- .../dashboard/graphql/types/dashboard.py | 5 +---- .../dashboard/tests/graphql/test_assignment.py | 17 +++++++---------- .../dashboard/tests/graphql/utils.py | 12 ++++++------ 3 files changed, 14 insertions(+), 20 deletions(-) diff --git a/server/vbv_lernwelt/dashboard/graphql/types/dashboard.py b/server/vbv_lernwelt/dashboard/graphql/types/dashboard.py index 2d1435c9..0e94fe2b 100644 --- a/server/vbv_lernwelt/dashboard/graphql/types/dashboard.py +++ b/server/vbv_lernwelt/dashboard/graphql/types/dashboard.py @@ -1,10 +1,7 @@ import graphene from vbv_lernwelt.course.models import CourseSession, CourseSessionUser -from vbv_lernwelt.dashboard.graphql.types.assignment import ( - Assignments, - assignments, -) +from vbv_lernwelt.dashboard.graphql.types.assignment import Assignments, assignments from vbv_lernwelt.dashboard.graphql.types.attendance import ( attendance_day_presences, AttendanceDayPresences, diff --git a/server/vbv_lernwelt/dashboard/tests/graphql/test_assignment.py b/server/vbv_lernwelt/dashboard/tests/graphql/test_assignment.py index 2df40247..8f22f855 100644 --- a/server/vbv_lernwelt/dashboard/tests/graphql/test_assignment.py +++ b/server/vbv_lernwelt/dashboard/tests/graphql/test_assignment.py @@ -3,26 +3,23 @@ from typing import Tuple from graphene_django.utils import GraphQLTestCase -from vbv_lernwelt.assignment.models import ( - AssignmentType, - Assignment, -) -from vbv_lernwelt.course.models import CourseSessionUser, CourseSession +from vbv_lernwelt.assignment.models import Assignment, AssignmentType +from vbv_lernwelt.course.models import CourseSession, CourseSessionUser from vbv_lernwelt.course_session.models import ( CourseSessionAssignment, CourseSessionEdoniqTest, ) from vbv_lernwelt.dashboard.tests.graphql.utils import ( add_course_session_user, - create_circle, - create_course, - create_course_session, - create_user, create_assignment, create_assignment_completion, create_assignment_learning_content, - create_course_session_edoniq_test, + create_circle, + create_course, + create_course_session, create_course_session_assignment, + create_course_session_edoniq_test, + create_user, ) from vbv_lernwelt.learnpath.models import Circle diff --git a/server/vbv_lernwelt/dashboard/tests/graphql/utils.py b/server/vbv_lernwelt/dashboard/tests/graphql/utils.py index e67621e5..f5921fa9 100644 --- a/server/vbv_lernwelt/dashboard/tests/graphql/utils.py +++ b/server/vbv_lernwelt/dashboard/tests/graphql/utils.py @@ -5,10 +5,10 @@ from django.contrib.auth.hashers import make_password from django.utils import timezone from vbv_lernwelt.assignment.models import ( - AssignmentType, - AssignmentCompletion, Assignment, + AssignmentCompletion, AssignmentCompletionStatus, + AssignmentType, ) from vbv_lernwelt.assignment.tests.assignment_factories import ( AssignmentFactory, @@ -32,25 +32,25 @@ from vbv_lernwelt.course.models import ( ) from vbv_lernwelt.course.utils import get_wagtail_default_site from vbv_lernwelt.course_session.models import ( - CourseSessionAttendanceCourse, CourseSessionAssignment, + CourseSessionAttendanceCourse, CourseSessionEdoniqTest, ) from vbv_lernwelt.duedate.models import DueDate from vbv_lernwelt.learnpath.models import ( Circle, - LearningPath, LearningContentAssignment, LearningContentEdoniqTest, + LearningPath, ) from vbv_lernwelt.learnpath.tests.learning_path_factories import ( CircleFactory, + LearningContentAssignmentFactory, LearningContentAttendanceCourseFactory, + LearningContentEdoniqTestFactory, LearningPathFactory, LearningUnitFactory, TopicFactory, - LearningContentAssignmentFactory, - LearningContentEdoniqTestFactory, ) From 61c57c4cb4fa8f3e66d0cb447a0d5de5550634a4 Mon Sep 17 00:00:00 2001 From: Livio Bieri Date: Mon, 23 Oct 2023 16:27:13 +0200 Subject: [PATCH 14/85] feat: removes supervisor role plus prod data fixes --- server/vbv_lernwelt/course/models.py | 1 - .../vbv_lernwelt/dashboard/graphql/queries.py | 19 +-- .../dashboard/graphql/types/assignment.py | 4 +- .../dashboard/graphql/types/attendance.py | 7 +- .../dashboard/graphql/types/competence.py | 14 +- .../dashboard/graphql/types/dashboard.py | 19 +-- .../dashboard/graphql/types/feedback.py | 12 +- .../tests/graphql/test_assignment.py | 15 +-- .../tests/graphql/test_attendance.py | 13 +- .../tests/graphql/test_competence.py | 17 +-- .../dashboard/tests/graphql/test_dashboard.py | 123 ++---------------- .../dashboard/tests/graphql/test_feedback.py | 19 +-- 12 files changed, 65 insertions(+), 198 deletions(-) diff --git a/server/vbv_lernwelt/course/models.py b/server/vbv_lernwelt/course/models.py index f735998f..4b6b1c2d 100644 --- a/server/vbv_lernwelt/course/models.py +++ b/server/vbv_lernwelt/course/models.py @@ -266,7 +266,6 @@ class CourseSessionUser(models.Model): class Role(models.TextChoices): MEMBER = "MEMBER", _("Teilnehmer") EXPERT = "EXPERT", _("Experte/Trainer") - SESSION_SUPERVISOR = "SESSION_SUPERVISOR", _("Regionalleiter") TUTOR = "TUTOR", _("Lernbegleitung") role = models.CharField(choices=Role.choices, max_length=255, default=Role.MEMBER) diff --git a/server/vbv_lernwelt/dashboard/graphql/queries.py b/server/vbv_lernwelt/dashboard/graphql/queries.py index fd508198..a708ca45 100644 --- a/server/vbv_lernwelt/dashboard/graphql/queries.py +++ b/server/vbv_lernwelt/dashboard/graphql/queries.py @@ -1,27 +1,22 @@ import graphene -from vbv_lernwelt.course.models import Course, CourseSessionUser -from vbv_lernwelt.dashboard.graphql.types.dashboard import CourseDashboardType +from vbv_lernwelt.course.models import Course +from vbv_lernwelt.dashboard.graphql.types.dashboard import CourseStatisticsType class DashboardQuery(graphene.ObjectType): - course_dashboard = graphene.List( - CourseDashboardType, course_id=graphene.ID(required=False) + course_statistics = graphene.List( + CourseStatisticsType, course_id=graphene.ID(required=True) ) - def resolve_course_dashboard(root, info, course_id: str | None = None): - user = info.context.user + def resolve_course_statistics(root, info, course_id: str): query = Course.objects.filter( - coursesession__coursesessionuser__user=user, - coursesession__coursesessionuser__role=CourseSessionUser.Role.SESSION_SUPERVISOR, + id=course_id ) - if course_id: - query = query.filter(id=course_id) - courses = query.distinct() return [ - CourseDashboardType(course_id=course.id, course_title=course.title) # noqa + CourseStatisticsType(course_id=course.id, course_title=course.title) # noqa for course in courses ] diff --git a/server/vbv_lernwelt/dashboard/graphql/types/assignment.py b/server/vbv_lernwelt/dashboard/graphql/types/assignment.py index 07ad0ead..d42d14ad 100644 --- a/server/vbv_lernwelt/dashboard/graphql/types/assignment.py +++ b/server/vbv_lernwelt/dashboard/graphql/types/assignment.py @@ -124,10 +124,8 @@ def create_record( ) -def assignments(course_id, user) -> Assignments: +def assignments(course_id) -> Assignments: course_sessions = CourseSession.objects.filter( - coursesessionuser__user=user, - coursesessionuser__role=CourseSessionUser.Role.SESSION_SUPERVISOR, course_id=course_id, ) diff --git a/server/vbv_lernwelt/dashboard/graphql/types/attendance.py b/server/vbv_lernwelt/dashboard/graphql/types/attendance.py index 3db5fa69..40620769 100644 --- a/server/vbv_lernwelt/dashboard/graphql/types/attendance.py +++ b/server/vbv_lernwelt/dashboard/graphql/types/attendance.py @@ -4,7 +4,6 @@ from typing import List import graphene from django.utils import timezone -from vbv_lernwelt.core.models import User from vbv_lernwelt.course.models import CourseSessionUser from vbv_lernwelt.course_session.models import CourseSessionAttendanceCourse from vbv_lernwelt.course_session.services.attendance import AttendanceUserStatus @@ -31,13 +30,9 @@ class AttendanceDayPresences(graphene.ObjectType): summary = graphene.Field(AttendanceSummary) -def attendance_day_presences( - course_id: graphene.String(), user: User -) -> AttendanceDayPresences: +def attendance_day_presences(course_id: graphene.String()) -> AttendanceDayPresences: completed = CourseSessionAttendanceCourse.objects.filter( course_session__course_id=course_id, - course_session__coursesessionuser__user=user, - course_session__coursesessionuser__role=CourseSessionUser.Role.SESSION_SUPERVISOR, due_date__end__lt=timezone.now(), ).order_by("-due_date__end") diff --git a/server/vbv_lernwelt/dashboard/graphql/types/competence.py b/server/vbv_lernwelt/dashboard/graphql/types/competence.py index adb3898f..aad79e28 100644 --- a/server/vbv_lernwelt/dashboard/graphql/types/competence.py +++ b/server/vbv_lernwelt/dashboard/graphql/types/competence.py @@ -1,10 +1,8 @@ import graphene -from vbv_lernwelt.core.models import User from vbv_lernwelt.course.models import ( CourseCompletion, CourseCompletionStatus, - CourseSessionUser, ) @@ -26,18 +24,22 @@ class Competences(graphene.ObjectType): summary = graphene.Field(CompletionSummary) -def competences(course_id: graphene.String(), user: User) -> Competences: +def competences(course_id: graphene.String()) -> Competences: completions = CourseCompletion.objects.filter( course_session__course_id=course_id, - course_session__coursesessionuser__user=user, - course_session__coursesessionuser__role=CourseSessionUser.Role.SESSION_SUPERVISOR, page_type="competence.PerformanceCriteria", ) competence_performances = {} + circle_cache = {} + for c in completions: - circle = c.page.specific.learning_unit.get_circle() + if c.page.id not in circle_cache: + circle_cache[c.page.id] = c.page.specific.learning_unit.get_circle() + + circle = circle_cache[c.page.id] + if circle.id not in competence_performances: competence_performances[circle.id] = CompetencePerformance( course_session_id=c.course_session.id, # noqa diff --git a/server/vbv_lernwelt/dashboard/graphql/types/dashboard.py b/server/vbv_lernwelt/dashboard/graphql/types/dashboard.py index 0e94fe2b..a69cdfc6 100644 --- a/server/vbv_lernwelt/dashboard/graphql/types/dashboard.py +++ b/server/vbv_lernwelt/dashboard/graphql/types/dashboard.py @@ -1,6 +1,6 @@ import graphene -from vbv_lernwelt.course.models import CourseSession, CourseSessionUser +from vbv_lernwelt.course.models import CourseSession from vbv_lernwelt.dashboard.graphql.types.assignment import Assignments, assignments from vbv_lernwelt.dashboard.graphql.types.attendance import ( attendance_day_presences, @@ -31,7 +31,7 @@ class CourseSessionProperties(graphene.ObjectType): circles = graphene.List(CircleData) -class CourseDashboardType(graphene.ObjectType): +class CourseStatisticsType(graphene.ObjectType): course_id = graphene.String() course_title = graphene.String() course_session_properties = graphene.Field(CourseSessionProperties) @@ -41,25 +41,26 @@ class CourseDashboardType(graphene.ObjectType): competences = graphene.Field(Competences) def resolve_attendance_day_presences(root, info) -> AttendanceDayPresences: - return attendance_day_presences(root.course_id, info.context.user) + return attendance_day_presences(root.course_id) def resolve_feedback_responses(root, info) -> FeedbackResponses: - return feedback_responses(root.course_id, info.context.user) + return feedback_responses(root.course_id) def resolve_competences(root, info) -> Competences: - return competences(root.course_id, info.context.user) + return competences(root.course_id) - def resolve_assignments(root, info): - return assignments(root.course_id, info.context.user) + def resolve_assignments(root, info) -> Assignments: + return assignments(root.course_id) def resolve_course_session_properties(root, info): course_session_data = [] circle_data = [] generations = set() + # TODO: Use course session group app to get all + # relevant course sessions for info.context.user + course_sessions = CourseSession.objects.filter( - coursesessionuser__user=info.context.user, - coursesessionuser__role=CourseSessionUser.Role.SESSION_SUPERVISOR, course_id=root.course_id, ) diff --git a/server/vbv_lernwelt/dashboard/graphql/types/feedback.py b/server/vbv_lernwelt/dashboard/graphql/types/feedback.py index 231e8803..c04199e5 100644 --- a/server/vbv_lernwelt/dashboard/graphql/types/feedback.py +++ b/server/vbv_lernwelt/dashboard/graphql/types/feedback.py @@ -2,8 +2,7 @@ from typing import List import graphene -from vbv_lernwelt.core.models import User -from vbv_lernwelt.course.models import CourseSession, CourseSessionUser +from vbv_lernwelt.course.models import CourseSession from vbv_lernwelt.feedback.models import FeedbackResponse from vbv_lernwelt.feedback.utils import feedback_users @@ -27,11 +26,9 @@ class FeedbackResponses(graphene.ObjectType): summary = graphene.Field(FeedbackSummary) -def feedback_responses(course_id: graphene.String(), user: User) -> FeedbackResponses: +def feedback_responses(course_id: graphene.String()) -> FeedbackResponses: # Get all course sessions for this user in the given course course_sessions = CourseSession.objects.filter( - coursesessionuser__user=user, - coursesessionuser__role=CourseSessionUser.Role.SESSION_SUPERVISOR, course_id=course_id, ) @@ -70,7 +67,10 @@ def circle_feedback_average( for fb in feedbacks: circle_id = fb.circle.id - satisfaction = fb.data.get("satisfaction", 0) + satisfaction = fb.data.get("satisfaction", None) + + if satisfaction is None: + continue if circle_id in circle_data: circle_data[circle_id]["total"] += satisfaction diff --git a/server/vbv_lernwelt/dashboard/tests/graphql/test_assignment.py b/server/vbv_lernwelt/dashboard/tests/graphql/test_assignment.py index 8f22f855..6151e4f0 100644 --- a/server/vbv_lernwelt/dashboard/tests/graphql/test_assignment.py +++ b/server/vbv_lernwelt/dashboard/tests/graphql/test_assignment.py @@ -26,8 +26,8 @@ from vbv_lernwelt.learnpath.models import Circle class AssignmentTestCase(GraphQLTestCase): GRAPHQL_URL = "/server/graphql/" - GRAPHQL_QUERY = f"""query($course_id: ID) {{ - course_dashboard(course_id: $course_id) {{ + GRAPHQL_QUERY = f"""query($course_id: ID!) {{ + course_statistics(course_id: $course_id) {{ assignments{{ summary{{ completed_count @@ -60,11 +60,6 @@ class AssignmentTestCase(GraphQLTestCase): self.circle, _ = create_circle(title="Circle", course_page=self.course_page) self.supervisor = create_user("supervisor") - add_course_session_user( - course_session=self.course_session, - user=self.supervisor, - role=CourseSessionUser.Role.SESSION_SUPERVISOR, - ) self.m1 = create_user("member_1") add_course_session_user( @@ -149,7 +144,7 @@ class AssignmentTestCase(GraphQLTestCase): # THEN self.assertResponseNoErrors(response) - dashboard = response.json()["data"]["course_dashboard"] + dashboard = response.json()["data"]["course_statistics"] records = dashboard[0]["assignments"]["records"] self.assertEqual(len(records), 1) @@ -191,7 +186,7 @@ class AssignmentTestCase(GraphQLTestCase): # THEN self.assertResponseNoErrors(response) - dashboard = response.json()["data"]["course_dashboard"] + dashboard = response.json()["data"]["course_statistics"] records = dashboard[0]["assignments"]["records"] self.assertEqual(len(records), 0) @@ -280,7 +275,7 @@ class AssignmentTestCase(GraphQLTestCase): # THEN self.assertResponseNoErrors(response) - dashboard = response.json()["data"]["course_dashboard"] + dashboard = response.json()["data"]["course_statistics"] # 1 -> incomplete (not counted for average) # 2 -> complete 66% passed ... diff --git a/server/vbv_lernwelt/dashboard/tests/graphql/test_attendance.py b/server/vbv_lernwelt/dashboard/tests/graphql/test_attendance.py index 5b490898..0148508a 100644 --- a/server/vbv_lernwelt/dashboard/tests/graphql/test_attendance.py +++ b/server/vbv_lernwelt/dashboard/tests/graphql/test_attendance.py @@ -23,14 +23,9 @@ class DashboardAttendanceTestCase(GraphQLTestCase): course, course_page = create_course("Test Course") course_session = create_course_session(course=course, title="Test Bern 2022 a") + # TODO: Give this guy the right permissions, once we have them ;) supervisor = create_user("supervisor") - add_course_session_user( - course_session=course_session, - user=supervisor, - role=CourseSessionUser.Role.SESSION_SUPERVISOR, - ) - circle, _ = create_circle(title="Test Circle", course_page=course_page) m1 = create_user("member_1") @@ -77,8 +72,8 @@ class DashboardAttendanceTestCase(GraphQLTestCase): self.client.force_login(supervisor) query = f""" - query($course_id: ID) {{ - course_dashboard(course_id: $course_id) {{ + query($course_id: ID!) {{ + course_statistics(course_id: $course_id) {{ attendance_day_presences{{ summary{{ days_completed @@ -105,7 +100,7 @@ class DashboardAttendanceTestCase(GraphQLTestCase): data = response.json()["data"] - attendance_day_presences = data["course_dashboard"][0][ + attendance_day_presences = data["course_statistics"][0][ "attendance_day_presences" ] diff --git a/server/vbv_lernwelt/dashboard/tests/graphql/test_competence.py b/server/vbv_lernwelt/dashboard/tests/graphql/test_competence.py index 5d91c1c1..047e54d6 100644 --- a/server/vbv_lernwelt/dashboard/tests/graphql/test_competence.py +++ b/server/vbv_lernwelt/dashboard/tests/graphql/test_competence.py @@ -20,14 +20,9 @@ class DashboardCompetenceTestCase(GraphQLTestCase): course, course_page = create_course("Test Course") course_session = create_course_session(course=course, title="Test Bern 2022 a") + # TODO: Give this guy the right permissions, once we have them ;) supervisor = create_user("supervisor") - add_course_session_user( - course_session=course_session, - user=supervisor, - role=CourseSessionUser.Role.SESSION_SUPERVISOR, - ) - member_one = create_user("member one") add_course_session_user( course_session=course_session, @@ -64,16 +59,16 @@ class DashboardCompetenceTestCase(GraphQLTestCase): self.client.force_login(supervisor) - query = f"""query($course_id: ID) {{ - course_dashboard(course_id: $course_id) {{ + query = f"""query($course_id: ID!) {{ + course_statistics(course_id: $course_id) {{ course_id competences {{ performances {{ course_session_id generation - circle_id + circle_id success_count - fail_count + fail_count }} summary {{ success_total @@ -91,7 +86,7 @@ class DashboardCompetenceTestCase(GraphQLTestCase): # THEN self.assertResponseNoErrors(response) - competences = response.json()["data"]["course_dashboard"][0]["competences"] + competences = response.json()["data"]["course_statistics"][0]["competences"] performances = competences["performances"] diff --git a/server/vbv_lernwelt/dashboard/tests/graphql/test_dashboard.py b/server/vbv_lernwelt/dashboard/tests/graphql/test_dashboard.py index afea64f4..cd86629e 100644 --- a/server/vbv_lernwelt/dashboard/tests/graphql/test_dashboard.py +++ b/server/vbv_lernwelt/dashboard/tests/graphql/test_dashboard.py @@ -1,9 +1,6 @@ from graphene_django.utils import GraphQLTestCase -from vbv_lernwelt.course.models import CourseSessionUser from vbv_lernwelt.dashboard.tests.graphql.utils import ( - add_course_session_user, - create_circle, create_course, create_course_session, create_user, @@ -13,121 +10,21 @@ from vbv_lernwelt.dashboard.tests.graphql.utils import ( class DashboardTestCase(GraphQLTestCase): GRAPHQL_URL = "/server/graphql/" - def test_course_dashboard(self): + def test_course_statistics_id(self): # GIVEN - supervisor = create_user("supervisor") - course, course_page = create_course("Test Course") - course_session = create_course_session(course=course, title="Test Bern 2022 a") - add_course_session_user( - course_session=course_session, - user=supervisor, - role=CourseSessionUser.Role.SESSION_SUPERVISOR, - ) - some_course, _ = create_course("Other Course") - some_course_session = create_course_session( - course=some_course, title="Here is go study" - ) - add_course_session_user( - course_session=some_course_session, - user=supervisor, - role=CourseSessionUser.Role.MEMBER, - ) - - circle, _ = create_circle(title="Test Circle", course_page=course_page) - - # expert - expert = create_user("expert") - expert_session_user = add_course_session_user( - course_session=some_course_session, - user=expert, - role=CourseSessionUser.Role.EXPERT, - ) - expert_session_user.expert.add(circle) - - self.client.force_login(supervisor) - - query = f""" - query {{ - course_dashboard {{ - course_id - course_title - course_session_properties {{ - sessions {{ - session_id - session_title - }} - generations - circles {{ - circle_id - circle_title - experts - }} - }} - }} - }} - """ - - # WHEN - response = self.query(query) - - # THEN - self.assertResponseNoErrors(response) - - course_dashboard = response.json()["data"]["course_dashboard"] - - self.assertEqual(len(course_dashboard), 1) - self.assertEqual(course_dashboard[0]["course_id"], str(course.id)) - self.assertEqual(course_dashboard[0]["course_title"], str(course.title)) - - session_properties = course_dashboard[0]["course_session_properties"] - self.assertEqual(len(session_properties["sessions"]), 1) - self.assertEqual( - session_properties["sessions"][0]["session_id"], str(course_session.id) - ) - self.assertEqual( - session_properties["sessions"][0]["session_title"], - str(course_session.title), - ) - - self.assertEqual(len(session_properties["generations"]), 1) - self.assertEqual( - session_properties["generations"][0], str(course_session.generation) - ) - - self.assertEqual(len(session_properties["circles"]), 1) - self.assertEqual(session_properties["circles"][0]["circle_id"], str(circle.id)) - self.assertEqual( - session_properties["circles"][0]["circle_title"], str(circle.title) - ) - self.assertEqual(session_properties["circles"][0]["experts"], ["Test Expert"]) - - def test_course_dashboard_id(self): - # GIVEN + # TODO: Give this guy the right permissions, once we have them supervisor = create_user("supervisor") course_1, _ = create_course("Test Course 1") course_2, _ = create_course("Test Course 2") - course_session_1 = create_course_session( - course=course_1, title="Test Course 1 Session" - ) - course_session_2 = create_course_session( - course=course_2, title="Test Course 2 Session" - ) - add_course_session_user( - course_session=course_session_1, - user=supervisor, - role=CourseSessionUser.Role.SESSION_SUPERVISOR, - ) - add_course_session_user( - course_session=course_session_2, - user=supervisor, - role=CourseSessionUser.Role.SESSION_SUPERVISOR, - ) + + create_course_session(course=course_1, title="Test Course 1 Session") + create_course_session(course=course_2, title="Test Course 2 Session") self.client.force_login(supervisor) - query = f"""query($course_id: ID) {{ - course_dashboard(course_id: $course_id) {{ + query = f"""query($course_id: ID!) {{ + course_statistics(course_id: $course_id) {{ course_id }} }} @@ -140,7 +37,7 @@ class DashboardTestCase(GraphQLTestCase): # THEN self.assertResponseNoErrors(response) - course_dashboard = response.json()["data"]["course_dashboard"] + course_statistics = response.json()["data"]["course_statistics"] - self.assertEqual(len(course_dashboard), 1) - self.assertEqual(course_dashboard[0]["course_id"], str(course_2.id)) + self.assertEqual(len(course_statistics), 1) + self.assertEqual(course_statistics[0]["course_id"], str(course_2.id)) diff --git a/server/vbv_lernwelt/dashboard/tests/graphql/test_feedback.py b/server/vbv_lernwelt/dashboard/tests/graphql/test_feedback.py index cef2b066..1158c05a 100644 --- a/server/vbv_lernwelt/dashboard/tests/graphql/test_feedback.py +++ b/server/vbv_lernwelt/dashboard/tests/graphql/test_feedback.py @@ -19,14 +19,9 @@ class DashboardFeedbackTestCase(GraphQLTestCase): course, course_page = create_course("Test Course") course_session = create_course_session(course=course, title="Test Bern 2022 a") + # TODO: Give this guy the right permissions, once we have them ;) supervisor = create_user("supervisor") - add_course_session_user( - course_session=course_session, - user=supervisor, - role=CourseSessionUser.Role.SESSION_SUPERVISOR, - ) - member = create_user("member") add_course_session_user( course_session=course_session, @@ -70,16 +65,16 @@ class DashboardFeedbackTestCase(GraphQLTestCase): self.client.force_login(supervisor) - query = f"""query($course_id: ID) {{ - course_dashboard(course_id: $course_id) {{ + query = f"""query($course_id: ID!) {{ + course_statistics(course_id: $course_id) {{ course_id feedback_responses {{ records {{ course_session_id generation - circle_id + circle_id satisfaction_average - satisfaction_max + satisfaction_max }} summary {{ satisfaction_average @@ -98,8 +93,8 @@ class DashboardFeedbackTestCase(GraphQLTestCase): # THEN self.assertResponseNoErrors(response) - course_dashboard = response.json()["data"]["course_dashboard"] - feedback_responses = course_dashboard[0]["feedback_responses"] + course_statistics = response.json()["data"]["course_statistics"] + feedback_responses = course_statistics[0]["feedback_responses"] records = feedback_responses["records"] self.assertEqual(len(records), 2) From c7920430caea3dd34feb2400b62a12fdfa93ebfb Mon Sep 17 00:00:00 2001 From: Reto Aebersold Date: Mon, 23 Oct 2023 16:55:26 +0200 Subject: [PATCH 15/85] feat: add course session group --- client/src/pages/DashPage.vue | 21 ++++++++ server/config/settings/base.py | 1 + .../course_session_group/__init__.py | 0 .../course_session_group/admin.py | 8 +++ .../vbv_lernwelt/course_session_group/apps.py | 9 ++++ .../migrations/0001_initial.py | 49 +++++++++++++++++++ .../migrations/__init__.py | 0 .../course_session_group/models.py | 25 ++++++++++ .../course_session_group/signals.py | 16 ++++++ .../course_session_group/tests.py | 3 ++ .../dashboard/graphql/types/competence.py | 5 +- 11 files changed, 133 insertions(+), 4 deletions(-) create mode 100644 client/src/pages/DashPage.vue create mode 100644 server/vbv_lernwelt/course_session_group/__init__.py create mode 100644 server/vbv_lernwelt/course_session_group/admin.py create mode 100644 server/vbv_lernwelt/course_session_group/apps.py create mode 100644 server/vbv_lernwelt/course_session_group/migrations/0001_initial.py create mode 100644 server/vbv_lernwelt/course_session_group/migrations/__init__.py create mode 100644 server/vbv_lernwelt/course_session_group/models.py create mode 100644 server/vbv_lernwelt/course_session_group/signals.py create mode 100644 server/vbv_lernwelt/course_session_group/tests.py diff --git a/client/src/pages/DashPage.vue b/client/src/pages/DashPage.vue new file mode 100644 index 00000000..eac1f744 --- /dev/null +++ b/client/src/pages/DashPage.vue @@ -0,0 +1,21 @@ + + + + + diff --git a/server/config/settings/base.py b/server/config/settings/base.py index a1f174d0..7e80cd01 100644 --- a/server/config/settings/base.py +++ b/server/config/settings/base.py @@ -129,6 +129,7 @@ LOCAL_APPS = [ "vbv_lernwelt.duedate", "vbv_lernwelt.importer", "vbv_lernwelt.edoniq_test", + "vbv_lernwelt.course_session_group", ] # https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + LOCAL_APPS diff --git a/server/vbv_lernwelt/course_session_group/__init__.py b/server/vbv_lernwelt/course_session_group/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/server/vbv_lernwelt/course_session_group/admin.py b/server/vbv_lernwelt/course_session_group/admin.py new file mode 100644 index 00000000..5bda3780 --- /dev/null +++ b/server/vbv_lernwelt/course_session_group/admin.py @@ -0,0 +1,8 @@ +from django.contrib import admin + +from vbv_lernwelt.course_session_group.models import CourseSessionGroup + + +@admin.register(CourseSessionGroup) +class CourseSessionAssignmentAdmin(admin.ModelAdmin): + ... diff --git a/server/vbv_lernwelt/course_session_group/apps.py b/server/vbv_lernwelt/course_session_group/apps.py new file mode 100644 index 00000000..5283ad81 --- /dev/null +++ b/server/vbv_lernwelt/course_session_group/apps.py @@ -0,0 +1,9 @@ +from django.apps import AppConfig + + +class CourseSessionGroupConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "vbv_lernwelt.course_session_group" + + def ready(self): + import vbv_lernwelt.course_session_group.signals # noqa F401 diff --git a/server/vbv_lernwelt/course_session_group/migrations/0001_initial.py b/server/vbv_lernwelt/course_session_group/migrations/0001_initial.py new file mode 100644 index 00000000..ca230768 --- /dev/null +++ b/server/vbv_lernwelt/course_session_group/migrations/0001_initial.py @@ -0,0 +1,49 @@ +# Generated by Django 3.2.20 on 2023-10-23 14:53 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + initial = True + + dependencies = [ + ("course", "0004_auto_20230823_1744"), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name="CourseSessionGroup", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=255)), + ( + "course", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="course.course" + ), + ), + ( + "course_session", + models.ManyToManyField(blank=True, to="course.CourseSession"), + ), + ( + "supervisor", + models.ManyToManyField(blank=True, to=settings.AUTH_USER_MODEL), + ), + ], + options={ + "ordering": ["name"], + }, + ), + ] diff --git a/server/vbv_lernwelt/course_session_group/migrations/__init__.py b/server/vbv_lernwelt/course_session_group/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/server/vbv_lernwelt/course_session_group/models.py b/server/vbv_lernwelt/course_session_group/models.py new file mode 100644 index 00000000..9ae705d3 --- /dev/null +++ b/server/vbv_lernwelt/course_session_group/models.py @@ -0,0 +1,25 @@ +from django.db import models + +from vbv_lernwelt.core.models import User + + +class CourseSessionGroup(models.Model): + name = models.CharField(max_length=255) + + course = models.ForeignKey("course.Course", on_delete=models.CASCADE) + + course_session = models.ManyToManyField( + "course.CourseSession", + blank=True, + ) + + supervisor = models.ManyToManyField( + User, + blank=True, + ) + + class Meta: + ordering = ["name"] + + def __str__(self): + return self.name diff --git a/server/vbv_lernwelt/course_session_group/signals.py b/server/vbv_lernwelt/course_session_group/signals.py new file mode 100644 index 00000000..3443e828 --- /dev/null +++ b/server/vbv_lernwelt/course_session_group/signals.py @@ -0,0 +1,16 @@ +from django.core.exceptions import ValidationError +from django.db.models.signals import m2m_changed +from django.dispatch import receiver + +from .models import CourseSessionGroup + + +@receiver(m2m_changed, sender=CourseSessionGroup.course_session.through) +def validate_course(sender, instance, action, reverse, model, pk_set, **kwargs): + if action == "pre_add": + course_sessions = model.objects.filter(pk__in=pk_set) + for session in course_sessions: + if session.course != instance.course: + raise ValidationError( + "CourseSession does not match the Course of this Group." + ) diff --git a/server/vbv_lernwelt/course_session_group/tests.py b/server/vbv_lernwelt/course_session_group/tests.py new file mode 100644 index 00000000..7ce503c2 --- /dev/null +++ b/server/vbv_lernwelt/course_session_group/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/server/vbv_lernwelt/dashboard/graphql/types/competence.py b/server/vbv_lernwelt/dashboard/graphql/types/competence.py index aad79e28..07823dd2 100644 --- a/server/vbv_lernwelt/dashboard/graphql/types/competence.py +++ b/server/vbv_lernwelt/dashboard/graphql/types/competence.py @@ -1,9 +1,6 @@ import graphene -from vbv_lernwelt.course.models import ( - CourseCompletion, - CourseCompletionStatus, -) +from vbv_lernwelt.course.models import CourseCompletion, CourseCompletionStatus class CompletionSummary(graphene.ObjectType): From ca44a913c97d66a285cab7e24ca5807732f084a1 Mon Sep 17 00:00:00 2001 From: Livio Bieri Date: Tue, 24 Oct 2023 12:05:55 +0200 Subject: [PATCH 16/85] wip: session group statistics w/ permission --- .../assignment/graphql/mutations.py | 2 +- .../vbv_lernwelt/assignment/graphql/types.py | 2 +- server/vbv_lernwelt/assignment/views.py | 2 +- server/vbv_lernwelt/course/graphql/queries.py | 2 +- server/vbv_lernwelt/course/graphql/types.py | 2 +- server/vbv_lernwelt/course/views.py | 15 ++-- .../course_session/graphql/mutations.py | 2 +- .../course_session/graphql/queries.py | 2 +- .../course_session/graphql/types.py | 2 +- server/vbv_lernwelt/course_session/views.py | 2 +- .../vbv_lernwelt/dashboard/graphql/queries.py | 68 +++++++++++++++---- .../dashboard/graphql/types/assignment.py | 6 +- .../dashboard/graphql/types/attendance.py | 6 +- .../dashboard/graphql/types/competence.py | 6 +- .../dashboard/graphql/types/dashboard.py | 17 +++-- .../dashboard/graphql/types/feedback.py | 6 +- .../dashboard/tests/graphql/test_dashboard.py | 61 +++++++++++++++++ .../dashboard/tests/graphql/utils.py | 14 ++++ server/vbv_lernwelt/edoniq_test/views.py | 2 +- .../feedback/graphql/mutations.py | 2 +- server/vbv_lernwelt/feedback/views.py | 2 +- server/vbv_lernwelt/iam/__init__.py | 0 .../{course => iam}/permissions.py | 48 ++++++++----- 23 files changed, 208 insertions(+), 63 deletions(-) create mode 100644 server/vbv_lernwelt/iam/__init__.py rename server/vbv_lernwelt/{course => iam}/permissions.py (65%) diff --git a/server/vbv_lernwelt/assignment/graphql/mutations.py b/server/vbv_lernwelt/assignment/graphql/mutations.py index 73b6de7f..2d9394ca 100644 --- a/server/vbv_lernwelt/assignment/graphql/mutations.py +++ b/server/vbv_lernwelt/assignment/graphql/mutations.py @@ -10,7 +10,7 @@ from vbv_lernwelt.assignment.models import Assignment, AssignmentCompletionStatu from vbv_lernwelt.assignment.services import update_assignment_completion from vbv_lernwelt.core.models import User from vbv_lernwelt.course.models import CourseSession, CourseSessionUser -from vbv_lernwelt.course.permissions import has_course_access, is_course_session_expert +from vbv_lernwelt.iam.permissions import has_course_access, is_course_session_expert logger = structlog.get_logger(__name__) diff --git a/server/vbv_lernwelt/assignment/graphql/types.py b/server/vbv_lernwelt/assignment/graphql/types.py index 33687377..021a2226 100644 --- a/server/vbv_lernwelt/assignment/graphql/types.py +++ b/server/vbv_lernwelt/assignment/graphql/types.py @@ -7,7 +7,7 @@ from vbv_lernwelt.assignment.models import Assignment, AssignmentCompletion from vbv_lernwelt.core.graphql.types import JSONStreamField from vbv_lernwelt.course.graphql.interfaces import CoursePageInterface from vbv_lernwelt.course.models import CourseSession -from vbv_lernwelt.course.permissions import has_course_access, is_course_session_expert +from vbv_lernwelt.iam.permissions import has_course_access, is_course_session_expert from vbv_lernwelt.learnpath.graphql.types import LearningContentInterface diff --git a/server/vbv_lernwelt/assignment/views.py b/server/vbv_lernwelt/assignment/views.py index 205c1321..f686ae51 100644 --- a/server/vbv_lernwelt/assignment/views.py +++ b/server/vbv_lernwelt/assignment/views.py @@ -4,7 +4,7 @@ from rest_framework.exceptions import PermissionDenied from rest_framework.response import Response from vbv_lernwelt.assignment.models import AssignmentCompletion -from vbv_lernwelt.course.permissions import is_course_session_expert +from vbv_lernwelt.iam.permissions import is_course_session_expert logger = structlog.get_logger(__name__) diff --git a/server/vbv_lernwelt/course/graphql/queries.py b/server/vbv_lernwelt/course/graphql/queries.py index b12b45e8..c81e0567 100644 --- a/server/vbv_lernwelt/course/graphql/queries.py +++ b/server/vbv_lernwelt/course/graphql/queries.py @@ -4,7 +4,7 @@ from graphql import GraphQLError from vbv_lernwelt.course.graphql.types import CourseObjectType, CourseSessionObjectType from vbv_lernwelt.course.models import Course, CourseSession -from vbv_lernwelt.course.permissions import has_course_access +from vbv_lernwelt.iam.permissions import has_course_access from vbv_lernwelt.learnpath.graphql.types import ( LearningContentAssignmentObjectType, LearningContentAttendanceCourseObjectType, diff --git a/server/vbv_lernwelt/course/graphql/types.py b/server/vbv_lernwelt/course/graphql/types.py index cb3883b2..923c5eed 100644 --- a/server/vbv_lernwelt/course/graphql/types.py +++ b/server/vbv_lernwelt/course/graphql/types.py @@ -16,7 +16,6 @@ from vbv_lernwelt.course.models import ( CourseSession, CourseSessionUser, ) -from vbv_lernwelt.course.permissions import has_course_access from vbv_lernwelt.course_session.graphql.types import ( CourseSessionAssignmentObjectType, CourseSessionAttendanceCourseObjectType, @@ -27,6 +26,7 @@ from vbv_lernwelt.course_session.models import ( CourseSessionAttendanceCourse, CourseSessionEdoniqTest, ) +from vbv_lernwelt.iam.permissions import has_course_access from vbv_lernwelt.learnpath.graphql.types import LearningPathObjectType logger = structlog.get_logger(__name__) diff --git a/server/vbv_lernwelt/course/views.py b/server/vbv_lernwelt/course/views.py index 3fce8b6b..627ab7a8 100644 --- a/server/vbv_lernwelt/course/views.py +++ b/server/vbv_lernwelt/course/views.py @@ -10,14 +10,6 @@ from vbv_lernwelt.course.models import ( CircleDocument, CourseCompletion, CourseSession, - CourseSessionUser, -) -from vbv_lernwelt.course.permissions import ( - course_sessions_for_user_qs, - has_course_access, - has_course_access_by_page_request, - is_circle_expert, - is_course_session_expert, ) from vbv_lernwelt.course.serializers import ( CourseCompletionSerializer, @@ -28,6 +20,13 @@ from vbv_lernwelt.course.serializers import ( from vbv_lernwelt.course.services import mark_course_completion from vbv_lernwelt.files.models import UploadFile from vbv_lernwelt.files.services import FileDirectUploadService +from vbv_lernwelt.iam.permissions import ( + has_course_access_by_page_request, + has_course_access, + is_course_session_expert, + course_sessions_for_user_qs, + is_circle_expert, +) logger = structlog.get_logger(__name__) diff --git a/server/vbv_lernwelt/course_session/graphql/mutations.py b/server/vbv_lernwelt/course_session/graphql/mutations.py index 189817db..1d00013b 100644 --- a/server/vbv_lernwelt/course_session/graphql/mutations.py +++ b/server/vbv_lernwelt/course_session/graphql/mutations.py @@ -2,7 +2,6 @@ import graphene import structlog from rest_framework.exceptions import PermissionDenied -from vbv_lernwelt.course.permissions import has_course_access from vbv_lernwelt.course_session.graphql.types import ( CourseSessionAttendanceCourseObjectType, ) @@ -11,6 +10,7 @@ from vbv_lernwelt.course_session.services.attendance import ( AttendanceUserStatus, update_attendance_list, ) +from vbv_lernwelt.iam.permissions import has_course_access logger = structlog.get_logger(__name__) diff --git a/server/vbv_lernwelt/course_session/graphql/queries.py b/server/vbv_lernwelt/course_session/graphql/queries.py index 899cd93a..33c544e4 100644 --- a/server/vbv_lernwelt/course_session/graphql/queries.py +++ b/server/vbv_lernwelt/course_session/graphql/queries.py @@ -2,11 +2,11 @@ import graphene from rest_framework.exceptions import PermissionDenied from vbv_lernwelt.course.models import CourseSession -from vbv_lernwelt.course.permissions import has_course_access, is_course_session_expert from vbv_lernwelt.course_session.graphql.types import ( CourseSessionAttendanceCourseObjectType, ) from vbv_lernwelt.course_session.models import CourseSessionAttendanceCourse +from vbv_lernwelt.iam.permissions import has_course_access, is_course_session_expert class CourseSessionQuery(object): diff --git a/server/vbv_lernwelt/course_session/graphql/types.py b/server/vbv_lernwelt/course_session/graphql/types.py index 56360a23..d4f4d3ac 100644 --- a/server/vbv_lernwelt/course_session/graphql/types.py +++ b/server/vbv_lernwelt/course_session/graphql/types.py @@ -1,7 +1,6 @@ import graphene from graphene_django import DjangoObjectType -from vbv_lernwelt.course.permissions import is_course_session_expert from vbv_lernwelt.course_session.models import ( CourseSessionAssignment, CourseSessionAttendanceCourse, @@ -9,6 +8,7 @@ from vbv_lernwelt.course_session.models import ( ) from vbv_lernwelt.course_session.services.attendance import AttendanceUserStatus from vbv_lernwelt.duedate.graphql.types import DueDateObjectType +from vbv_lernwelt.iam.permissions import is_course_session_expert from vbv_lernwelt.learnpath.graphql.types import ( LearningContentAssignmentObjectType, LearningContentAttendanceCourseObjectType, diff --git a/server/vbv_lernwelt/course_session/views.py b/server/vbv_lernwelt/course_session/views.py index 6cf3dbfc..ac4af629 100644 --- a/server/vbv_lernwelt/course_session/views.py +++ b/server/vbv_lernwelt/course_session/views.py @@ -3,8 +3,8 @@ from rest_framework.exceptions import PermissionDenied from rest_framework.response import Response from vbv_lernwelt.course.models import CircleDocument -from vbv_lernwelt.course.permissions import has_course_session_access from vbv_lernwelt.course.serializers import CircleDocumentSerializer +from vbv_lernwelt.iam.permissions import has_course_session_access @api_view(["GET"]) diff --git a/server/vbv_lernwelt/dashboard/graphql/queries.py b/server/vbv_lernwelt/dashboard/graphql/queries.py index a708ca45..ae4c31cf 100644 --- a/server/vbv_lernwelt/dashboard/graphql/queries.py +++ b/server/vbv_lernwelt/dashboard/graphql/queries.py @@ -1,22 +1,64 @@ import graphene -from vbv_lernwelt.course.models import Course -from vbv_lernwelt.dashboard.graphql.types.dashboard import CourseStatisticsType +from vbv_lernwelt.course.models import CourseSession, Course +from vbv_lernwelt.course_session_group.models import CourseSessionGroup +from vbv_lernwelt.dashboard.graphql.types.dashboard import CourseStatisticsType, DashboardConfigType +from vbv_lernwelt.iam.permissions import can_view_course_session_group_statistics, can_view_course_session class DashboardQuery(graphene.ObjectType): - course_statistics = graphene.List( - CourseStatisticsType, course_id=graphene.ID(required=True) + course_statistics = graphene.Field(CourseStatisticsType, course_id=graphene.ID(required=True)) + + dashboard_config = graphene.List( + DashboardConfigType ) - def resolve_course_statistics(root, info, course_id: str): - query = Course.objects.filter( - id=course_id - ) + def resolve_course_statistics(root, info, course_id: str): # noqa + user = info.context.user + course = Course.objects.get(id=course_id) - courses = query.distinct() + course_session_ids = set() - return [ - CourseStatisticsType(course_id=course.id, course_title=course.title) # noqa - for course in courses - ] + for group in CourseSessionGroup.objects.filter(course=course): + if can_view_course_session_group_statistics(user=user, group=group): + course_session_ids.update(group.course_session.all().values_list("id", flat=True)) + + if not course_session_ids: + return None + + + return CourseStatisticsType(course_id=course.id, course_title=course.title, # noqa + course_session_selection_ids=list(course_session_ids)) # noqa + + + + def resolve_dashboard_config(root, info): # noqa + user = info.context.user + + course_index = set() + dashboards = [] + + for group in CourseSessionGroup.objects.all(): + if can_view_course_session_group_statistics(user=user, group=group): + course = group.course + course_index.add(course) + dashboards.append( + { + "id": str(course.id), + "title": course.title, + "dashboard_type": "StatisticsDashboard", + } + ) + + for course_session in CourseSession.objects.exclude(course__in=course_index): + if can_view_course_session(user=user, course_session=course_session): + course = course_session.course + dashboards.append( + { + "id": str(course.id), + "title": course.title, + "dashboard_type": "SimpleDashboard", + } + ) + + return dashboards diff --git a/server/vbv_lernwelt/dashboard/graphql/types/assignment.py b/server/vbv_lernwelt/dashboard/graphql/types/assignment.py index d42d14ad..8baa650b 100644 --- a/server/vbv_lernwelt/dashboard/graphql/types/assignment.py +++ b/server/vbv_lernwelt/dashboard/graphql/types/assignment.py @@ -124,9 +124,11 @@ def create_record( ) -def assignments(course_id) -> Assignments: +def assignments( + course_session_selection_ids: graphene.List(graphene.ID), +) -> Assignments: course_sessions = CourseSession.objects.filter( - course_id=course_id, + id_in=course_session_selection_ids, ) records: List[AssignmentRecord] = [] diff --git a/server/vbv_lernwelt/dashboard/graphql/types/attendance.py b/server/vbv_lernwelt/dashboard/graphql/types/attendance.py index 40620769..01b41ac4 100644 --- a/server/vbv_lernwelt/dashboard/graphql/types/attendance.py +++ b/server/vbv_lernwelt/dashboard/graphql/types/attendance.py @@ -30,9 +30,11 @@ class AttendanceDayPresences(graphene.ObjectType): summary = graphene.Field(AttendanceSummary) -def attendance_day_presences(course_id: graphene.String()) -> AttendanceDayPresences: +def attendance_day_presences( + course_session_selection_ids: graphene.List(graphene.ID), +) -> AttendanceDayPresences: completed = CourseSessionAttendanceCourse.objects.filter( - course_session__course_id=course_id, + course_session_id__in=course_session_selection_ids, due_date__end__lt=timezone.now(), ).order_by("-due_date__end") diff --git a/server/vbv_lernwelt/dashboard/graphql/types/competence.py b/server/vbv_lernwelt/dashboard/graphql/types/competence.py index 07823dd2..42826e0a 100644 --- a/server/vbv_lernwelt/dashboard/graphql/types/competence.py +++ b/server/vbv_lernwelt/dashboard/graphql/types/competence.py @@ -21,9 +21,11 @@ class Competences(graphene.ObjectType): summary = graphene.Field(CompletionSummary) -def competences(course_id: graphene.String()) -> Competences: +def competences( + course_session_selection_ids: graphene.List(graphene.ID), +) -> Competences: completions = CourseCompletion.objects.filter( - course_session__course_id=course_id, + course_session_id__in=course_session_selection_ids, page_type="competence.PerformanceCriteria", ) diff --git a/server/vbv_lernwelt/dashboard/graphql/types/dashboard.py b/server/vbv_lernwelt/dashboard/graphql/types/dashboard.py index a69cdfc6..23392429 100644 --- a/server/vbv_lernwelt/dashboard/graphql/types/dashboard.py +++ b/server/vbv_lernwelt/dashboard/graphql/types/dashboard.py @@ -31,9 +31,16 @@ class CourseSessionProperties(graphene.ObjectType): circles = graphene.List(CircleData) +class DashboardConfigType(graphene.ObjectType): + id = graphene.ID() + title = graphene.String() + dashboard_type = graphene.String() + + class CourseStatisticsType(graphene.ObjectType): - course_id = graphene.String() + course_id = graphene.ID() course_title = graphene.String() + course_session_selection_ids = graphene.List(graphene.ID) course_session_properties = graphene.Field(CourseSessionProperties) attendance_day_presences = graphene.Field(AttendanceDayPresences) feedback_responses = graphene.Field(FeedbackResponses) @@ -41,16 +48,16 @@ class CourseStatisticsType(graphene.ObjectType): competences = graphene.Field(Competences) def resolve_attendance_day_presences(root, info) -> AttendanceDayPresences: - return attendance_day_presences(root.course_id) + return attendance_day_presences(root.course_session_selection_ids) def resolve_feedback_responses(root, info) -> FeedbackResponses: - return feedback_responses(root.course_id) + return feedback_responses(root.course_session_selection_ids) def resolve_competences(root, info) -> Competences: - return competences(root.course_id) + return competences(root.course_session_selection_ids) def resolve_assignments(root, info) -> Assignments: - return assignments(root.course_id) + return assignments(root.course_session_selection_ids) def resolve_course_session_properties(root, info): course_session_data = [] diff --git a/server/vbv_lernwelt/dashboard/graphql/types/feedback.py b/server/vbv_lernwelt/dashboard/graphql/types/feedback.py index c04199e5..1a1b35c9 100644 --- a/server/vbv_lernwelt/dashboard/graphql/types/feedback.py +++ b/server/vbv_lernwelt/dashboard/graphql/types/feedback.py @@ -26,10 +26,12 @@ class FeedbackResponses(graphene.ObjectType): summary = graphene.Field(FeedbackSummary) -def feedback_responses(course_id: graphene.String()) -> FeedbackResponses: +def feedback_responses( + course_session_selection_ids: graphene.List(graphene.ID), +) -> FeedbackResponses: # Get all course sessions for this user in the given course course_sessions = CourseSession.objects.filter( - course_id=course_id, + id_in=course_session_selection_ids, ) circle_feedbacks = [] diff --git a/server/vbv_lernwelt/dashboard/tests/graphql/test_dashboard.py b/server/vbv_lernwelt/dashboard/tests/graphql/test_dashboard.py index cd86629e..08bd44c1 100644 --- a/server/vbv_lernwelt/dashboard/tests/graphql/test_dashboard.py +++ b/server/vbv_lernwelt/dashboard/tests/graphql/test_dashboard.py @@ -1,15 +1,70 @@ from graphene_django.utils import GraphQLTestCase +from vbv_lernwelt.course.models import CourseSessionUser from vbv_lernwelt.dashboard.tests.graphql.utils import ( create_course, create_course_session, create_user, + add_course_session_user, + create_course_session_group, ) class DashboardTestCase(GraphQLTestCase): GRAPHQL_URL = "/server/graphql/" + def test_dashboard_config(self): + # GIVEN + course_1, _ = create_course("Test Course 1") + course_2, _ = create_course("Test Course 2") + + cs_1 = create_course_session(course=course_1, title="Test Course 1 Session") + cs_2 = create_course_session(course=course_2, title="Test Course 2 Session") + + # Member of course 1 (via cs_1) + # Supervisor of course 2 (via cs_2) + supervisor = create_user("supervisor") + + add_course_session_user( + course_session=cs_1, user=supervisor, role=CourseSessionUser.Role.MEMBER + ) + + create_course_session_group(course_session=cs_2, user=supervisor) + + self.client.force_login(supervisor) + + # WHEN + query = """query { + dashboard_config { + id + title + dashboard_type + } + } + """ + + response = self.query(query) + + # THEN + self.assertResponseNoErrors(response) + + dashboard_config = response.json()["data"]["dashboard_config"] + self.assertEqual(len(dashboard_config), 2) + + course_1_config = find_dashboard_config_by_course_id( + dashboard_config, course_1.id + ) + self.assertIsNotNone(course_1_config) + self.assertEqual(course_1_config["title"], course_1.title) + self.assertEqual(course_1_config["dashboard_type"], "SimpleDashboard") + + course_2_config = find_dashboard_config_by_course_id( + dashboard_config, course_2.id + ) + self.assertIsNotNone(course_2_config) + self.assertEqual(course_2_config["title"], course_2.title) + self.assertEqual(course_2_config["dashboard_type"], "StatisticsDashboard") + def test_course_statistics_id(self): # GIVEN @@ -41,3 +96,9 @@ class DashboardTestCase(GraphQLTestCase): self.assertEqual(len(course_statistics), 1) self.assertEqual(course_statistics[0]["course_id"], str(course_2.id)) + + +def find_dashboard_config_by_course_id(dashboard_configs, course_id): + return next( + (config for config in dashboard_configs if config["id"] == str(course_id)), None + ) diff --git a/server/vbv_lernwelt/dashboard/tests/graphql/utils.py b/server/vbv_lernwelt/dashboard/tests/graphql/utils.py index f5921fa9..c1625e25 100644 --- a/server/vbv_lernwelt/dashboard/tests/graphql/utils.py +++ b/server/vbv_lernwelt/dashboard/tests/graphql/utils.py @@ -36,6 +36,7 @@ from vbv_lernwelt.course_session.models import ( CourseSessionAttendanceCourse, CourseSessionEdoniqTest, ) +from vbv_lernwelt.course_session_group.models import CourseSessionGroup from vbv_lernwelt.duedate.models import DueDate from vbv_lernwelt.learnpath.models import ( Circle, @@ -99,6 +100,19 @@ def add_course_session_user( ) +def create_course_session_group( + course_session: CourseSession, user: User +) -> CourseSessionGroup: + g = CourseSessionGroup.objects.create( + course=course_session.course, + ) + + g.course_session.add(course_session) + g.supervisor.add(user) + + return g + + def create_circle( title: str, course_page: CoursePage, learning_path: LearningPath | None = None ) -> Tuple[Circle, LearningPath]: diff --git a/server/vbv_lernwelt/edoniq_test/views.py b/server/vbv_lernwelt/edoniq_test/views.py index f70dec12..00d8ff7b 100644 --- a/server/vbv_lernwelt/edoniq_test/views.py +++ b/server/vbv_lernwelt/edoniq_test/views.py @@ -14,8 +14,8 @@ from rest_framework.decorators import api_view from vbv_lernwelt.core.models import User from vbv_lernwelt.course.consts import COURSE_UK, COURSE_UK_FR, COURSE_UK_IT from vbv_lernwelt.course.models import CourseSessionUser -from vbv_lernwelt.course.permissions import has_course_access_by_page_request from vbv_lernwelt.edoniq_test.edoniq_sso import create_token +from vbv_lernwelt.iam.permissions import has_course_access_by_page_request from vbv_lernwelt.learnpath.models import LearningContentEdoniqTest logger = structlog.get_logger(__name__) diff --git a/server/vbv_lernwelt/feedback/graphql/mutations.py b/server/vbv_lernwelt/feedback/graphql/mutations.py index 9a569d39..e36ad651 100644 --- a/server/vbv_lernwelt/feedback/graphql/mutations.py +++ b/server/vbv_lernwelt/feedback/graphql/mutations.py @@ -4,12 +4,12 @@ from graphene.types.generic import GenericScalar from graphene_django.types import ErrorType from vbv_lernwelt.course.models import CourseSession -from vbv_lernwelt.course.permissions import has_course_session_access from vbv_lernwelt.feedback.graphql.types import ( FeedbackResponseObjectType as FeedbackResponseType, ) from vbv_lernwelt.feedback.serializers import CourseFeedbackSerializer from vbv_lernwelt.feedback.services import update_feedback_response +from vbv_lernwelt.iam.permissions import has_course_session_access from vbv_lernwelt.learnpath.models import LearningContentFeedback logger = structlog.get_logger(__name__) diff --git a/server/vbv_lernwelt/feedback/views.py b/server/vbv_lernwelt/feedback/views.py index ebfd27cf..7d3bb27f 100644 --- a/server/vbv_lernwelt/feedback/views.py +++ b/server/vbv_lernwelt/feedback/views.py @@ -5,9 +5,9 @@ from rest_framework.decorators import api_view from rest_framework.exceptions import PermissionDenied from rest_framework.response import Response -from vbv_lernwelt.course.permissions import is_course_session_expert from vbv_lernwelt.feedback.models import FeedbackResponse from vbv_lernwelt.feedback.utils import feedback_users +from vbv_lernwelt.iam.permissions import is_course_session_expert logger = structlog.get_logger(__name__) diff --git a/server/vbv_lernwelt/iam/__init__.py b/server/vbv_lernwelt/iam/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/server/vbv_lernwelt/course/permissions.py b/server/vbv_lernwelt/iam/permissions.py similarity index 65% rename from server/vbv_lernwelt/course/permissions.py rename to server/vbv_lernwelt/iam/permissions.py index 35e79b7f..fb75507e 100644 --- a/server/vbv_lernwelt/course/permissions.py +++ b/server/vbv_lernwelt/iam/permissions.py @@ -1,4 +1,6 @@ +from vbv_lernwelt.core.models import User from vbv_lernwelt.course.models import CourseSession, CourseSessionUser +from vbv_lernwelt.course_session_group.models import CourseSessionGroup from vbv_lernwelt.learnpath.models import LearningSequence @@ -10,38 +12,29 @@ def has_course_access(user, course_id): if user.is_superuser: return True - if CourseSessionUser.objects.filter( + return CourseSessionUser.objects.filter( course_session__course_id=course_id, user=user - ).exists(): - return True - - return False + ).exists() def has_course_session_access(user, course_session_id: int): if user.is_superuser: return True - if CourseSessionUser.objects.filter( + return CourseSessionUser.objects.filter( course_session_id=course_session_id, user=user - ).exists(): - return True - - return False + ).exists() def is_course_session_expert(user, course_session_id: int): if user.is_superuser: return True - if CourseSessionUser.objects.filter( + return CourseSessionUser.objects.filter( course_session_id=course_session_id, user=user, role=CourseSessionUser.Role.EXPERT, - ).exists(): - return True - - return False + ).exists() def course_sessions_for_user_qs(user): @@ -64,12 +57,33 @@ def is_circle_expert(user, course_session_id: int, learning_sequence_id: int) -> circle_id = learning_sequence.get_parent().circle.id - if CourseSessionUser.objects.filter( + return CourseSessionUser.objects.filter( course_session_id=course_session_id, user=user, role=CourseSessionUser.Role.EXPERT, expert__id=circle_id, + ).exists() + + +def can_view_course_session_group_statistics( + user: User, group: CourseSessionGroup +) -> bool: + if user.is_superuser: + return True + + return user in group.supervisor.all() + + +def can_view_course_session(user: User, course_session: CourseSession) -> bool: + if user.is_superuser: + return True + + if CourseSessionGroup.objects.filter( + course_session=course_session, supervisor=user ).exists(): return True - return False + return CourseSessionUser.objects.filter( + course_session=course_session, + user=user, + ).exists() From 5665ffdee023685ca84576605f79f2ca09f3b7e8 Mon Sep 17 00:00:00 2001 From: Livio Bieri Date: Tue, 24 Oct 2023 13:52:52 +0200 Subject: [PATCH 17/85] followup: fixes the dashboard / statistics tests --- .../dashboard/graphql/types/assignment.py | 2 +- .../dashboard/graphql/types/feedback.py | 2 +- .../dashboard/tests/graphql/test_assignment.py | 13 +++++++++---- .../dashboard/tests/graphql/test_attendance.py | 7 +++---- .../dashboard/tests/graphql/test_competence.py | 6 +++--- .../dashboard/tests/graphql/test_dashboard.py | 13 +++++++------ .../dashboard/tests/graphql/test_feedback.py | 5 +++-- 7 files changed, 27 insertions(+), 21 deletions(-) diff --git a/server/vbv_lernwelt/dashboard/graphql/types/assignment.py b/server/vbv_lernwelt/dashboard/graphql/types/assignment.py index 8baa650b..91a05e89 100644 --- a/server/vbv_lernwelt/dashboard/graphql/types/assignment.py +++ b/server/vbv_lernwelt/dashboard/graphql/types/assignment.py @@ -128,7 +128,7 @@ def assignments( course_session_selection_ids: graphene.List(graphene.ID), ) -> Assignments: course_sessions = CourseSession.objects.filter( - id_in=course_session_selection_ids, + id__in=course_session_selection_ids, ) records: List[AssignmentRecord] = [] diff --git a/server/vbv_lernwelt/dashboard/graphql/types/feedback.py b/server/vbv_lernwelt/dashboard/graphql/types/feedback.py index 1a1b35c9..3f078208 100644 --- a/server/vbv_lernwelt/dashboard/graphql/types/feedback.py +++ b/server/vbv_lernwelt/dashboard/graphql/types/feedback.py @@ -31,7 +31,7 @@ def feedback_responses( ) -> FeedbackResponses: # Get all course sessions for this user in the given course course_sessions = CourseSession.objects.filter( - id_in=course_session_selection_ids, + id__in=course_session_selection_ids, ) circle_feedbacks = [] diff --git a/server/vbv_lernwelt/dashboard/tests/graphql/test_assignment.py b/server/vbv_lernwelt/dashboard/tests/graphql/test_assignment.py index 6151e4f0..f82ae426 100644 --- a/server/vbv_lernwelt/dashboard/tests/graphql/test_assignment.py +++ b/server/vbv_lernwelt/dashboard/tests/graphql/test_assignment.py @@ -20,6 +20,7 @@ from vbv_lernwelt.dashboard.tests.graphql.utils import ( create_course_session_assignment, create_course_session_edoniq_test, create_user, + create_course_session_group, ) from vbv_lernwelt.learnpath.models import Circle @@ -61,6 +62,10 @@ class AssignmentTestCase(GraphQLTestCase): self.supervisor = create_user("supervisor") + create_course_session_group( + course_session=self.course_session, user=self.supervisor + ) + self.m1 = create_user("member_1") add_course_session_user( course_session=self.course_session, @@ -146,7 +151,7 @@ class AssignmentTestCase(GraphQLTestCase): self.assertResponseNoErrors(response) dashboard = response.json()["data"]["course_statistics"] - records = dashboard[0]["assignments"]["records"] + records = dashboard["assignments"]["records"] self.assertEqual(len(records), 1) record = records[0] @@ -188,7 +193,7 @@ class AssignmentTestCase(GraphQLTestCase): self.assertResponseNoErrors(response) dashboard = response.json()["data"]["course_statistics"] - records = dashboard[0]["assignments"]["records"] + records = dashboard["assignments"]["records"] self.assertEqual(len(records), 0) def test_metrics_summary(self): @@ -281,11 +286,11 @@ class AssignmentTestCase(GraphQLTestCase): # 2 -> complete 66% passed ... # 3 -> complete 100% passed --> 83.5% # 4 -> incomplete (not counted for average) - summary = dashboard[0]["assignments"]["summary"] + summary = dashboard["assignments"]["summary"] self.assertEqual(summary["completed_count"], 2) self.assertEqual(summary["average_passed"], 83.5) - records = dashboard[0]["assignments"]["records"] + records = dashboard["assignments"]["records"] self.assertEqual(len(records), 4) # 1 -> assigment_1_results (oldest) diff --git a/server/vbv_lernwelt/dashboard/tests/graphql/test_attendance.py b/server/vbv_lernwelt/dashboard/tests/graphql/test_attendance.py index 0148508a..6c0a9eca 100644 --- a/server/vbv_lernwelt/dashboard/tests/graphql/test_attendance.py +++ b/server/vbv_lernwelt/dashboard/tests/graphql/test_attendance.py @@ -12,6 +12,7 @@ from vbv_lernwelt.dashboard.tests.graphql.utils import ( create_course, create_course_session, create_user, + create_course_session_group, ) @@ -23,8 +24,8 @@ class DashboardAttendanceTestCase(GraphQLTestCase): course, course_page = create_course("Test Course") course_session = create_course_session(course=course, title="Test Bern 2022 a") - # TODO: Give this guy the right permissions, once we have them ;) supervisor = create_user("supervisor") + create_course_session_group(course_session=course_session, user=supervisor) circle, _ = create_circle(title="Test Circle", course_page=course_page) @@ -100,9 +101,7 @@ class DashboardAttendanceTestCase(GraphQLTestCase): data = response.json()["data"] - attendance_day_presences = data["course_statistics"][0][ - "attendance_day_presences" - ] + attendance_day_presences = data["course_statistics"]["attendance_day_presences"] record = attendance_day_presences["records"][0] diff --git a/server/vbv_lernwelt/dashboard/tests/graphql/test_competence.py b/server/vbv_lernwelt/dashboard/tests/graphql/test_competence.py index 047e54d6..c12d1392 100644 --- a/server/vbv_lernwelt/dashboard/tests/graphql/test_competence.py +++ b/server/vbv_lernwelt/dashboard/tests/graphql/test_competence.py @@ -9,6 +9,7 @@ from vbv_lernwelt.dashboard.tests.graphql.utils import ( create_course_session, create_performance_criteria_page, create_user, + create_course_session_group, ) @@ -20,8 +21,8 @@ class DashboardCompetenceTestCase(GraphQLTestCase): course, course_page = create_course("Test Course") course_session = create_course_session(course=course, title="Test Bern 2022 a") - # TODO: Give this guy the right permissions, once we have them ;) supervisor = create_user("supervisor") + create_course_session_group(course_session=course_session, user=supervisor) member_one = create_user("member one") add_course_session_user( @@ -86,8 +87,7 @@ class DashboardCompetenceTestCase(GraphQLTestCase): # THEN self.assertResponseNoErrors(response) - competences = response.json()["data"]["course_statistics"][0]["competences"] - + competences = response.json()["data"]["course_statistics"]["competences"] performances = competences["performances"] self.assertEqual(performances[0]["success_count"], 1) diff --git a/server/vbv_lernwelt/dashboard/tests/graphql/test_dashboard.py b/server/vbv_lernwelt/dashboard/tests/graphql/test_dashboard.py index 08bd44c1..6812e0b4 100644 --- a/server/vbv_lernwelt/dashboard/tests/graphql/test_dashboard.py +++ b/server/vbv_lernwelt/dashboard/tests/graphql/test_dashboard.py @@ -73,8 +73,11 @@ class DashboardTestCase(GraphQLTestCase): course_1, _ = create_course("Test Course 1") course_2, _ = create_course("Test Course 2") - create_course_session(course=course_1, title="Test Course 1 Session") - create_course_session(course=course_2, title="Test Course 2 Session") + cs_1 = create_course_session(course=course_1, title="Test Course 1 Session") + cs_2 = create_course_session(course=course_2, title="Test Course 2 Session") + + create_course_session_group(course_session=cs_1, user=supervisor) + create_course_session_group(course_session=cs_2, user=supervisor) self.client.force_login(supervisor) @@ -83,7 +86,7 @@ class DashboardTestCase(GraphQLTestCase): course_id }} }} - """ + """ variables = {"course_id": str(course_2.id)} # WHEN @@ -93,9 +96,7 @@ class DashboardTestCase(GraphQLTestCase): self.assertResponseNoErrors(response) course_statistics = response.json()["data"]["course_statistics"] - - self.assertEqual(len(course_statistics), 1) - self.assertEqual(course_statistics[0]["course_id"], str(course_2.id)) + self.assertEqual(course_statistics["course_id"], str(course_2.id)) def find_dashboard_config_by_course_id(dashboard_configs, course_id): diff --git a/server/vbv_lernwelt/dashboard/tests/graphql/test_feedback.py b/server/vbv_lernwelt/dashboard/tests/graphql/test_feedback.py index 1158c05a..475376b0 100644 --- a/server/vbv_lernwelt/dashboard/tests/graphql/test_feedback.py +++ b/server/vbv_lernwelt/dashboard/tests/graphql/test_feedback.py @@ -7,6 +7,7 @@ from vbv_lernwelt.dashboard.tests.graphql.utils import ( create_course, create_course_session, create_user, + create_course_session_group, ) from vbv_lernwelt.feedback.models import FeedbackResponse @@ -19,8 +20,8 @@ class DashboardFeedbackTestCase(GraphQLTestCase): course, course_page = create_course("Test Course") course_session = create_course_session(course=course, title="Test Bern 2022 a") - # TODO: Give this guy the right permissions, once we have them ;) supervisor = create_user("supervisor") + create_course_session_group(course_session=course_session, user=supervisor) member = create_user("member") add_course_session_user( @@ -94,7 +95,7 @@ class DashboardFeedbackTestCase(GraphQLTestCase): self.assertResponseNoErrors(response) course_statistics = response.json()["data"]["course_statistics"] - feedback_responses = course_statistics[0]["feedback_responses"] + feedback_responses = course_statistics["feedback_responses"] records = feedback_responses["records"] self.assertEqual(len(records), 2) From 067c7ac20d2738e787f4499994f1c29d20802a9b Mon Sep 17 00:00:00 2001 From: Livio Bieri Date: Tue, 24 Oct 2023 14:01:36 +0200 Subject: [PATCH 18/85] chore: test course statistics permission --- .../dashboard/tests/graphql/test_dashboard.py | 27 +++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/server/vbv_lernwelt/dashboard/tests/graphql/test_dashboard.py b/server/vbv_lernwelt/dashboard/tests/graphql/test_dashboard.py index 6812e0b4..09fe9309 100644 --- a/server/vbv_lernwelt/dashboard/tests/graphql/test_dashboard.py +++ b/server/vbv_lernwelt/dashboard/tests/graphql/test_dashboard.py @@ -65,10 +65,33 @@ class DashboardTestCase(GraphQLTestCase): self.assertEqual(course_2_config["title"], course_2.title) self.assertEqual(course_2_config["dashboard_type"], "StatisticsDashboard") + def test_course_statistics_deny_not_allowed_users(self): + # GIVEN + disallowed_user = create_user("1337_hacker_schorsch") + course, _ = create_course("Test Course") + create_course_session(course=course, title="Test Course Session") + + self.client.force_login(disallowed_user) + + query = f"""query($course_id: ID!) {{ + course_statistics(course_id: $course_id) {{ + course_id + }} + }} + """ + variables = {"course_id": str(course.id)} + + # WHEN + response = self.query(query, variables=variables) + + # THEN + self.assertResponseNoErrors(response) + + course_statistics = response.json()["data"]["course_statistics"] + self.assertEqual(course_statistics, None) + def test_course_statistics_id(self): # GIVEN - - # TODO: Give this guy the right permissions, once we have them supervisor = create_user("supervisor") course_1, _ = create_course("Test Course 1") course_2, _ = create_course("Test Course 2") From d41bdf84e3917ca66934d2415e13d830754e9ffa Mon Sep 17 00:00:00 2001 From: Livio Bieri Date: Tue, 24 Oct 2023 17:02:19 +0200 Subject: [PATCH 19/85] chore: expose selection metrics (participant / session / expert count) --- .../dashboard/graphql/types/dashboard.py | 41 ++++++- .../tests/graphql/test_assignment.py | 6 +- .../tests/graphql/test_attendance.py | 5 +- .../tests/graphql/test_competence.py | 4 +- .../dashboard/tests/graphql/test_dashboard.py | 18 +++- .../dashboard/tests/graphql/test_feedback.py | 5 +- .../tests/graphql/test_selection_metrics.py | 101 ++++++++++++++++++ .../dashboard/tests/graphql/utils.py | 27 +++-- 8 files changed, 182 insertions(+), 25 deletions(-) create mode 100644 server/vbv_lernwelt/dashboard/tests/graphql/test_selection_metrics.py diff --git a/server/vbv_lernwelt/dashboard/graphql/types/dashboard.py b/server/vbv_lernwelt/dashboard/graphql/types/dashboard.py index 23392429..df652341 100644 --- a/server/vbv_lernwelt/dashboard/graphql/types/dashboard.py +++ b/server/vbv_lernwelt/dashboard/graphql/types/dashboard.py @@ -1,6 +1,6 @@ import graphene -from vbv_lernwelt.course.models import CourseSession +from vbv_lernwelt.course.models import CourseSession, CourseSessionUser from vbv_lernwelt.dashboard.graphql.types.assignment import Assignments, assignments from vbv_lernwelt.dashboard.graphql.types.attendance import ( attendance_day_presences, @@ -25,6 +25,12 @@ class CircleData(graphene.ObjectType): experts = graphene.List(graphene.String) +class CourseSessionsSelectionMetrics(graphene.ObjectType): + session_count = graphene.Int(required=True) + participant_count = graphene.Int(required=True) + expert_count = graphene.Int(required=True) + + class CourseSessionProperties(graphene.ObjectType): sessions = graphene.List(CourseSessionData) generations = graphene.List(graphene.String) @@ -40,8 +46,9 @@ class DashboardConfigType(graphene.ObjectType): class CourseStatisticsType(graphene.ObjectType): course_id = graphene.ID() course_title = graphene.String() - course_session_selection_ids = graphene.List(graphene.ID) course_session_properties = graphene.Field(CourseSessionProperties) + course_session_selection_ids = graphene.List(graphene.ID) + course_session_selection_metrics = graphene.Field(CourseSessionsSelectionMetrics) attendance_day_presences = graphene.Field(AttendanceDayPresences) feedback_responses = graphene.Field(FeedbackResponses) assignments = graphene.Field(Assignments) @@ -59,15 +66,39 @@ class CourseStatisticsType(graphene.ObjectType): def resolve_assignments(root, info) -> Assignments: return assignments(root.course_session_selection_ids) + def resolve_course_session_selection_metrics( + root, info + ) -> CourseSessionsSelectionMetrics: + course_session_count = CourseSession.objects.filter( + id__in=root.course_session_selection_ids, + course_id=root.course_id, + ).count() + + expert_count = CourseSession.objects.filter( + id__in=root.course_session_selection_ids, + course_id=root.course_id, + coursesessionuser__role=CourseSessionUser.Role.EXPERT, + ).count() + + participant_count = CourseSession.objects.filter( + id__in=root.course_session_selection_ids, + course_id=root.course_id, + coursesessionuser__role=CourseSessionUser.Role.MEMBER, + ).count() + + return CourseSessionsSelectionMetrics( + session_count=course_session_count, # noqa + participant_count=participant_count, # noqa + expert_count=expert_count, # noqa + ) + def resolve_course_session_properties(root, info): course_session_data = [] circle_data = [] generations = set() - # TODO: Use course session group app to get all - # relevant course sessions for info.context.user - course_sessions = CourseSession.objects.filter( + id__in=root.course_session_selection_ids, course_id=root.course_id, ) diff --git a/server/vbv_lernwelt/dashboard/tests/graphql/test_assignment.py b/server/vbv_lernwelt/dashboard/tests/graphql/test_assignment.py index f82ae426..1dede516 100644 --- a/server/vbv_lernwelt/dashboard/tests/graphql/test_assignment.py +++ b/server/vbv_lernwelt/dashboard/tests/graphql/test_assignment.py @@ -21,6 +21,7 @@ from vbv_lernwelt.dashboard.tests.graphql.utils import ( create_course_session_edoniq_test, create_user, create_course_session_group, + add_course_session_group_supervisor, ) from vbv_lernwelt.learnpath.models import Circle @@ -62,9 +63,8 @@ class AssignmentTestCase(GraphQLTestCase): self.supervisor = create_user("supervisor") - create_course_session_group( - course_session=self.course_session, user=self.supervisor - ) + group = create_course_session_group(course_session=self.course_session) + add_course_session_group_supervisor(group=group, user=self.supervisor) self.m1 = create_user("member_1") add_course_session_user( diff --git a/server/vbv_lernwelt/dashboard/tests/graphql/test_attendance.py b/server/vbv_lernwelt/dashboard/tests/graphql/test_attendance.py index 6c0a9eca..17fce53a 100644 --- a/server/vbv_lernwelt/dashboard/tests/graphql/test_attendance.py +++ b/server/vbv_lernwelt/dashboard/tests/graphql/test_attendance.py @@ -13,6 +13,7 @@ from vbv_lernwelt.dashboard.tests.graphql.utils import ( create_course_session, create_user, create_course_session_group, + add_course_session_group_supervisor, ) @@ -25,7 +26,9 @@ class DashboardAttendanceTestCase(GraphQLTestCase): course_session = create_course_session(course=course, title="Test Bern 2022 a") supervisor = create_user("supervisor") - create_course_session_group(course_session=course_session, user=supervisor) + + group = create_course_session_group(course_session=course_session) + add_course_session_group_supervisor(group=group, user=supervisor) circle, _ = create_circle(title="Test Circle", course_page=course_page) diff --git a/server/vbv_lernwelt/dashboard/tests/graphql/test_competence.py b/server/vbv_lernwelt/dashboard/tests/graphql/test_competence.py index c12d1392..0dc423d1 100644 --- a/server/vbv_lernwelt/dashboard/tests/graphql/test_competence.py +++ b/server/vbv_lernwelt/dashboard/tests/graphql/test_competence.py @@ -10,6 +10,7 @@ from vbv_lernwelt.dashboard.tests.graphql.utils import ( create_performance_criteria_page, create_user, create_course_session_group, + add_course_session_group_supervisor, ) @@ -22,7 +23,8 @@ class DashboardCompetenceTestCase(GraphQLTestCase): course_session = create_course_session(course=course, title="Test Bern 2022 a") supervisor = create_user("supervisor") - create_course_session_group(course_session=course_session, user=supervisor) + group = create_course_session_group(course_session=course_session) + add_course_session_group_supervisor(group=group, user=supervisor) member_one = create_user("member one") add_course_session_user( diff --git a/server/vbv_lernwelt/dashboard/tests/graphql/test_dashboard.py b/server/vbv_lernwelt/dashboard/tests/graphql/test_dashboard.py index 09fe9309..238f11c9 100644 --- a/server/vbv_lernwelt/dashboard/tests/graphql/test_dashboard.py +++ b/server/vbv_lernwelt/dashboard/tests/graphql/test_dashboard.py @@ -7,6 +7,7 @@ from vbv_lernwelt.dashboard.tests.graphql.utils import ( create_user, add_course_session_user, create_course_session_group, + add_course_session_group_supervisor, ) @@ -29,7 +30,8 @@ class DashboardTestCase(GraphQLTestCase): course_session=cs_1, user=supervisor, role=CourseSessionUser.Role.MEMBER ) - create_course_session_group(course_session=cs_2, user=supervisor) + group = create_course_session_group(course_session=cs_2) + add_course_session_group_supervisor(group=group, user=supervisor) self.client.force_login(supervisor) @@ -65,7 +67,7 @@ class DashboardTestCase(GraphQLTestCase): self.assertEqual(course_2_config["title"], course_2.title) self.assertEqual(course_2_config["dashboard_type"], "StatisticsDashboard") - def test_course_statistics_deny_not_allowed_users(self): + def test_course_statistics_deny_not_allowed_user(self): # GIVEN disallowed_user = create_user("1337_hacker_schorsch") course, _ = create_course("Test Course") @@ -90,7 +92,7 @@ class DashboardTestCase(GraphQLTestCase): course_statistics = response.json()["data"]["course_statistics"] self.assertEqual(course_statistics, None) - def test_course_statistics_id(self): + def test_course_statistics_data(self): # GIVEN supervisor = create_user("supervisor") course_1, _ = create_course("Test Course 1") @@ -99,14 +101,18 @@ class DashboardTestCase(GraphQLTestCase): cs_1 = create_course_session(course=course_1, title="Test Course 1 Session") cs_2 = create_course_session(course=course_2, title="Test Course 2 Session") - create_course_session_group(course_session=cs_1, user=supervisor) - create_course_session_group(course_session=cs_2, user=supervisor) + cs_group_1 = create_course_session_group(course_session=cs_1) + add_course_session_group_supervisor(group=cs_group_1, user=supervisor) + + cs_group_2 = create_course_session_group(course_session=cs_2) + add_course_session_group_supervisor(group=cs_group_2, user=supervisor) self.client.force_login(supervisor) query = f"""query($course_id: ID!) {{ course_statistics(course_id: $course_id) {{ course_id + course_title }} }} """ @@ -119,7 +125,9 @@ class DashboardTestCase(GraphQLTestCase): self.assertResponseNoErrors(response) course_statistics = response.json()["data"]["course_statistics"] + self.assertEqual(course_statistics["course_id"], str(course_2.id)) + self.assertEqual(course_statistics["course_title"], course_2.title) def find_dashboard_config_by_course_id(dashboard_configs, course_id): diff --git a/server/vbv_lernwelt/dashboard/tests/graphql/test_feedback.py b/server/vbv_lernwelt/dashboard/tests/graphql/test_feedback.py index 475376b0..5ab9c9dd 100644 --- a/server/vbv_lernwelt/dashboard/tests/graphql/test_feedback.py +++ b/server/vbv_lernwelt/dashboard/tests/graphql/test_feedback.py @@ -8,6 +8,7 @@ from vbv_lernwelt.dashboard.tests.graphql.utils import ( create_course_session, create_user, create_course_session_group, + add_course_session_group_supervisor, ) from vbv_lernwelt.feedback.models import FeedbackResponse @@ -21,7 +22,9 @@ class DashboardFeedbackTestCase(GraphQLTestCase): course_session = create_course_session(course=course, title="Test Bern 2022 a") supervisor = create_user("supervisor") - create_course_session_group(course_session=course_session, user=supervisor) + + group = create_course_session_group(course_session=course_session) + add_course_session_group_supervisor(group=group, user=supervisor) member = create_user("member") add_course_session_user( diff --git a/server/vbv_lernwelt/dashboard/tests/graphql/test_selection_metrics.py b/server/vbv_lernwelt/dashboard/tests/graphql/test_selection_metrics.py new file mode 100644 index 00000000..bd2665da --- /dev/null +++ b/server/vbv_lernwelt/dashboard/tests/graphql/test_selection_metrics.py @@ -0,0 +1,101 @@ +from graphene_django.utils import GraphQLTestCase + +from vbv_lernwelt.course.models import CourseSessionUser +from vbv_lernwelt.dashboard.tests.graphql.utils import ( + create_course, + create_course_session, + create_user, + add_course_session_user, + create_course_session_group, + add_course_session_group_supervisor, + add_course_session_group_course_session, +) + + +class DashboardTestCase(GraphQLTestCase): + GRAPHQL_URL = "/server/graphql/" + + def test_selection_metrics(self): + # GIVEN + course_1, _ = create_course("Test Course 1") + course_2, _ = create_course("Dummy Course 2") + + cs_1_a = create_course_session(course=course_1, title="Zug", generation="1984") + cs_1_b = create_course_session(course=course_1, title="Bern", generation="1984") + cs_1_c = create_course_session(course=course_1, title="Wil", generation="1984") + cs_2_a = create_course_session(course=course_2, title="Baar", generation="1984") + + member_1 = create_user("member_1") + member_2 = create_user("member_2") + member_3 = create_user("member_3") + member_4 = create_user("member_4") + + expert_1 = create_user("expert_1") + expert_2 = create_user("expert_2") + expert_3 = create_user("expert_3") + expert_4 = create_user("expert_4") + + # CS 1 A + add_course_session_user( + course_session=cs_1_a, user=member_1, role=CourseSessionUser.Role.MEMBER + ) + add_course_session_user( + course_session=cs_1_b, user=member_2, role=CourseSessionUser.Role.MEMBER + ) + + # CS 1 B + add_course_session_user( + course_session=cs_1_a, user=expert_1, role=CourseSessionUser.Role.EXPERT + ) + add_course_session_user( + course_session=cs_1_b, user=expert_2, role=CourseSessionUser.Role.EXPERT + ) + + # CS 1 C + add_course_session_user( + course_session=cs_1_c, user=member_3, role=CourseSessionUser.Role.MEMBER + ) + add_course_session_user( + course_session=cs_1_c, user=expert_3, role=CourseSessionUser.Role.EXPERT + ) + + # CS 2 A + add_course_session_user( + course_session=cs_2_a, user=member_4, role=CourseSessionUser.Role.MEMBER + ) + add_course_session_user( + course_session=cs_2_a, user=expert_4, role=CourseSessionUser.Role.EXPERT + ) + + # SUPERVISOR of course 1, session a and b BUT NOT + # of course 1, session c or course 2, session a + cs_1_ab_supervisor = create_user("supervisor") + group = create_course_session_group(course_session=cs_1_a) + add_course_session_group_course_session(course_session=cs_1_b, group=group) + add_course_session_group_supervisor(group=group, user=cs_1_ab_supervisor) + + self.client.force_login(cs_1_ab_supervisor) + + # WHEN + query = f"""query($course_id: ID!) {{ + course_statistics(course_id: $course_id) {{ + course_session_selection_metrics {{ + expert_count + participant_count + session_count + }} + }} + }}""" + + variables = {"course_id": str(course_1.id)} + response = self.query(query, variables=variables) + + # THEN + self.assertResponseNoErrors(response) + + metrics = response.json()["data"]["course_statistics"][ + "course_session_selection_metrics" + ] + self.assertEqual(metrics["expert_count"], 2) + self.assertEqual(metrics["participant_count"], 2) + self.assertEqual(metrics["session_count"], 2) diff --git a/server/vbv_lernwelt/dashboard/tests/graphql/utils.py b/server/vbv_lernwelt/dashboard/tests/graphql/utils.py index c1625e25..65a5901f 100644 --- a/server/vbv_lernwelt/dashboard/tests/graphql/utils.py +++ b/server/vbv_lernwelt/dashboard/tests/graphql/utils.py @@ -80,12 +80,14 @@ def create_user(username: str) -> User: ) -def create_course_session(course: Course, title: str) -> CourseSession: +def create_course_session( + course: Course, title: str, generation: str = "2023" +) -> CourseSession: return CourseSession.objects.create( course=course, title=title, import_id=title, - generation="2023", + generation=generation, start_date=timezone.now(), ) @@ -100,17 +102,24 @@ def add_course_session_user( ) -def create_course_session_group( - course_session: CourseSession, user: User -) -> CourseSessionGroup: - g = CourseSessionGroup.objects.create( +def create_course_session_group(course_session: CourseSession) -> CourseSessionGroup: + group = CourseSessionGroup.objects.create( course=course_session.course, ) - g.course_session.add(course_session) - g.supervisor.add(user) + group.course_session.add(course_session) - return g + return group + + +def add_course_session_group_supervisor(group: CourseSessionGroup, user: User): + group.supervisor.add(user) + + +def add_course_session_group_course_session( + group: CourseSessionGroup, course_session: CourseSession +): + group.course_session.add(course_session) def create_circle( From 49fdbd9648b0933093019809f343152298a4c329 Mon Sep 17 00:00:00 2001 From: Livio Bieri Date: Wed, 25 Oct 2023 10:46:58 +0200 Subject: [PATCH 20/85] fix: expose id on top-level; use name for dashboard config --- .../vbv_lernwelt/dashboard/graphql/queries.py | 30 ++++++---- .../dashboard/graphql/types/competence.py | 18 +++--- .../dashboard/graphql/types/dashboard.py | 57 +++++++++++-------- .../dashboard/graphql/types/feedback.py | 20 +++---- .../tests/graphql/test_competence.py | 2 +- .../dashboard/tests/graphql/test_dashboard.py | 18 +++--- .../dashboard/tests/graphql/test_feedback.py | 2 +- server/vbv_lernwelt/iam/permissions.py | 8 +++ 8 files changed, 89 insertions(+), 66 deletions(-) diff --git a/server/vbv_lernwelt/dashboard/graphql/queries.py b/server/vbv_lernwelt/dashboard/graphql/queries.py index ae4c31cf..59d102d6 100644 --- a/server/vbv_lernwelt/dashboard/graphql/queries.py +++ b/server/vbv_lernwelt/dashboard/graphql/queries.py @@ -2,16 +2,14 @@ import graphene from vbv_lernwelt.course.models import CourseSession, Course from vbv_lernwelt.course_session_group.models import CourseSessionGroup -from vbv_lernwelt.dashboard.graphql.types.dashboard import CourseStatisticsType, DashboardConfigType -from vbv_lernwelt.iam.permissions import can_view_course_session_group_statistics, can_view_course_session +from vbv_lernwelt.dashboard.graphql.types.dashboard import CourseStatisticsType, DashboardConfigType, DashboardType +from vbv_lernwelt.iam.permissions import can_view_course_session_group_statistics, can_view_course_session, \ + can_view_course_session_progress class DashboardQuery(graphene.ObjectType): course_statistics = graphene.Field(CourseStatisticsType, course_id=graphene.ID(required=True)) - - dashboard_config = graphene.List( - DashboardConfigType - ) + dashboard_config = graphene.List(DashboardConfigType, required=False) def resolve_course_statistics(root, info, course_id: str): # noqa user = info.context.user @@ -27,7 +25,7 @@ class DashboardQuery(graphene.ObjectType): return None - return CourseStatisticsType(course_id=course.id, course_title=course.title, # noqa + return CourseStatisticsType(id=course.id, course_title=course.title, # noqa course_session_selection_ids=list(course_session_ids)) # noqa @@ -45,19 +43,27 @@ class DashboardQuery(graphene.ObjectType): dashboards.append( { "id": str(course.id), - "title": course.title, - "dashboard_type": "StatisticsDashboard", + "name": course.title, + "dashboard_type": DashboardType.STATISTICS_DASHBOARD } ) for course_session in CourseSession.objects.exclude(course__in=course_index): + course = course_session.course if can_view_course_session(user=user, course_session=course_session): - course = course_session.course dashboards.append( { "id": str(course.id), - "title": course.title, - "dashboard_type": "SimpleDashboard", + "name": course.title, + "dashboard_type": DashboardType.SIMPLE_LIST_DASHBOARD + } + ) + if can_view_course_session_progress(user=user, course_session=course_session): + dashboards.append( + { + "id": str(course.id), + "name": course.title, + "dashboard_type": DashboardType.PROGRESS_DASHBOARD } ) diff --git a/server/vbv_lernwelt/dashboard/graphql/types/competence.py b/server/vbv_lernwelt/dashboard/graphql/types/competence.py index 42826e0a..469bce3a 100644 --- a/server/vbv_lernwelt/dashboard/graphql/types/competence.py +++ b/server/vbv_lernwelt/dashboard/graphql/types/competence.py @@ -4,21 +4,21 @@ from vbv_lernwelt.course.models import CourseCompletion, CourseCompletionStatus class CompletionSummary(graphene.ObjectType): - success_total = graphene.Int() - fail_total = graphene.Int() + success_total = graphene.Int(required=True) + fail_total = graphene.Int(required=True) class CompetencePerformance(graphene.ObjectType): - course_session_id = graphene.ID() - generation = graphene.String() - circle_id = graphene.ID() - success_count = graphene.Int() - fail_count = graphene.Int() + course_session_id = graphene.ID(required=True) + generation = graphene.String(required=True) + circle_id = graphene.ID(required=True) + success_count = graphene.Int(required=True) + fail_count = graphene.Int(required=True) class Competences(graphene.ObjectType): - performances = graphene.List(CompetencePerformance) - summary = graphene.Field(CompletionSummary) + performances = graphene.List(CompetencePerformance, required=True) + summary = graphene.Field(CompletionSummary, required=True) def competences( diff --git a/server/vbv_lernwelt/dashboard/graphql/types/dashboard.py b/server/vbv_lernwelt/dashboard/graphql/types/dashboard.py index df652341..eb2a8ee9 100644 --- a/server/vbv_lernwelt/dashboard/graphql/types/dashboard.py +++ b/server/vbv_lernwelt/dashboard/graphql/types/dashboard.py @@ -1,4 +1,5 @@ import graphene +from graphene import Enum from vbv_lernwelt.course.models import CourseSession, CourseSessionUser from vbv_lernwelt.dashboard.graphql.types.assignment import Assignments, assignments @@ -15,14 +16,14 @@ from vbv_lernwelt.learnpath.models import Circle class CourseSessionData(graphene.ObjectType): - session_id = graphene.ID() - session_title = graphene.String() + session_id = graphene.ID(required=True) + session_title = graphene.String(required=True) class CircleData(graphene.ObjectType): - circle_id = graphene.ID() - circle_title = graphene.String() - experts = graphene.List(graphene.String) + circle_id = graphene.ID(required=True) + circle_title = graphene.String(required=True) + experts = graphene.List(graphene.String, required=True) class CourseSessionsSelectionMetrics(graphene.ObjectType): @@ -32,27 +33,35 @@ class CourseSessionsSelectionMetrics(graphene.ObjectType): class CourseSessionProperties(graphene.ObjectType): - sessions = graphene.List(CourseSessionData) - generations = graphene.List(graphene.String) - circles = graphene.List(CircleData) + sessions = graphene.List(CourseSessionData, required=True) + generations = graphene.List(graphene.String, required=True) + circles = graphene.List(CircleData, required=True) + + +class DashboardType(Enum): + STATISTICS_DASHBOARD = "StatisticsDashboard" + PROGRESS_DASHBOARD = "ProgressDashboard" + SIMPLE_LIST_DASHBOARD = "SimpleListDashboard" class DashboardConfigType(graphene.ObjectType): - id = graphene.ID() - title = graphene.String() - dashboard_type = graphene.String() + id = graphene.ID(required=True) + name = graphene.String(required=True) + dashboard_type = graphene.Field(DashboardType, required=True) class CourseStatisticsType(graphene.ObjectType): - course_id = graphene.ID() - course_title = graphene.String() - course_session_properties = graphene.Field(CourseSessionProperties) - course_session_selection_ids = graphene.List(graphene.ID) - course_session_selection_metrics = graphene.Field(CourseSessionsSelectionMetrics) - attendance_day_presences = graphene.Field(AttendanceDayPresences) - feedback_responses = graphene.Field(FeedbackResponses) - assignments = graphene.Field(Assignments) - competences = graphene.Field(Competences) + id = graphene.ID(required=True) + course_title = graphene.String(required=True) + course_session_properties = graphene.Field(CourseSessionProperties, required=True) + course_session_selection_ids = graphene.List(graphene.ID, required=True) + course_session_selection_metrics = graphene.Field( + CourseSessionsSelectionMetrics, required=True + ) + attendance_day_presences = graphene.Field(AttendanceDayPresences, required=True) + feedback_responses = graphene.Field(FeedbackResponses, required=True) + assignments = graphene.Field(Assignments, required=True) + competences = graphene.Field(Competences, required=True) def resolve_attendance_day_presences(root, info) -> AttendanceDayPresences: return attendance_day_presences(root.course_session_selection_ids) @@ -71,18 +80,18 @@ class CourseStatisticsType(graphene.ObjectType): ) -> CourseSessionsSelectionMetrics: course_session_count = CourseSession.objects.filter( id__in=root.course_session_selection_ids, - course_id=root.course_id, + course_id=root.id, ).count() expert_count = CourseSession.objects.filter( id__in=root.course_session_selection_ids, - course_id=root.course_id, + course_id=root.id, coursesessionuser__role=CourseSessionUser.Role.EXPERT, ).count() participant_count = CourseSession.objects.filter( id__in=root.course_session_selection_ids, - course_id=root.course_id, + course_id=root.id, coursesessionuser__role=CourseSessionUser.Role.MEMBER, ).count() @@ -99,7 +108,7 @@ class CourseStatisticsType(graphene.ObjectType): course_sessions = CourseSession.objects.filter( id__in=root.course_session_selection_ids, - course_id=root.course_id, + course_id=root.id, ) for course_session in course_sessions: diff --git a/server/vbv_lernwelt/dashboard/graphql/types/feedback.py b/server/vbv_lernwelt/dashboard/graphql/types/feedback.py index 3f078208..d768685c 100644 --- a/server/vbv_lernwelt/dashboard/graphql/types/feedback.py +++ b/server/vbv_lernwelt/dashboard/graphql/types/feedback.py @@ -8,22 +8,22 @@ from vbv_lernwelt.feedback.utils import feedback_users class FeedbackSummary(graphene.ObjectType): - satisfaction_average = graphene.Float() - satisfaction_max = graphene.Int() - total_responses = graphene.Int() + satisfaction_average = graphene.Float(required=True) + satisfaction_max = graphene.Int(required=True) + total_responses = graphene.Int(required=True) class FeedbackRecord(graphene.ObjectType): - course_session_id = graphene.ID() - generation = graphene.String() - circle_id = graphene.ID() - satisfaction_average = graphene.Float() - satisfaction_max = graphene.Int() + course_session_id = graphene.ID(required=True) + generation = graphene.String(required=True) + circle_id = graphene.ID(required=True) + satisfaction_average = graphene.Float(required=True) + satisfaction_max = graphene.Int(required=True) class FeedbackResponses(graphene.ObjectType): - records = graphene.List(FeedbackRecord) - summary = graphene.Field(FeedbackSummary) + records = graphene.List(FeedbackRecord, required=True) + summary = graphene.Field(FeedbackSummary, required=True) def feedback_responses( diff --git a/server/vbv_lernwelt/dashboard/tests/graphql/test_competence.py b/server/vbv_lernwelt/dashboard/tests/graphql/test_competence.py index 0dc423d1..f2ce8f03 100644 --- a/server/vbv_lernwelt/dashboard/tests/graphql/test_competence.py +++ b/server/vbv_lernwelt/dashboard/tests/graphql/test_competence.py @@ -64,7 +64,7 @@ class DashboardCompetenceTestCase(GraphQLTestCase): query = f"""query($course_id: ID!) {{ course_statistics(course_id: $course_id) {{ - course_id + id competences {{ performances {{ course_session_id diff --git a/server/vbv_lernwelt/dashboard/tests/graphql/test_dashboard.py b/server/vbv_lernwelt/dashboard/tests/graphql/test_dashboard.py index 238f11c9..ceb28fd7 100644 --- a/server/vbv_lernwelt/dashboard/tests/graphql/test_dashboard.py +++ b/server/vbv_lernwelt/dashboard/tests/graphql/test_dashboard.py @@ -39,7 +39,7 @@ class DashboardTestCase(GraphQLTestCase): query = """query { dashboard_config { id - title + name dashboard_type } } @@ -51,21 +51,21 @@ class DashboardTestCase(GraphQLTestCase): self.assertResponseNoErrors(response) dashboard_config = response.json()["data"]["dashboard_config"] - self.assertEqual(len(dashboard_config), 2) + self.assertEqual(len(dashboard_config), 3) course_1_config = find_dashboard_config_by_course_id( dashboard_config, course_1.id ) self.assertIsNotNone(course_1_config) - self.assertEqual(course_1_config["title"], course_1.title) - self.assertEqual(course_1_config["dashboard_type"], "SimpleDashboard") + self.assertEqual(course_1_config["name"], course_1.title) + self.assertEqual(course_1_config["dashboard_type"], "SIMPLE_LIST_DASHBOARD") course_2_config = find_dashboard_config_by_course_id( dashboard_config, course_2.id ) self.assertIsNotNone(course_2_config) - self.assertEqual(course_2_config["title"], course_2.title) - self.assertEqual(course_2_config["dashboard_type"], "StatisticsDashboard") + self.assertEqual(course_2_config["name"], course_2.title) + self.assertEqual(course_2_config["dashboard_type"], "STATISTICS_DASHBOARD") def test_course_statistics_deny_not_allowed_user(self): # GIVEN @@ -77,7 +77,7 @@ class DashboardTestCase(GraphQLTestCase): query = f"""query($course_id: ID!) {{ course_statistics(course_id: $course_id) {{ - course_id + id }} }} """ @@ -111,7 +111,7 @@ class DashboardTestCase(GraphQLTestCase): query = f"""query($course_id: ID!) {{ course_statistics(course_id: $course_id) {{ - course_id + id course_title }} }} @@ -126,7 +126,7 @@ class DashboardTestCase(GraphQLTestCase): course_statistics = response.json()["data"]["course_statistics"] - self.assertEqual(course_statistics["course_id"], str(course_2.id)) + self.assertEqual(course_statistics["id"], str(course_2.id)) self.assertEqual(course_statistics["course_title"], course_2.title) diff --git a/server/vbv_lernwelt/dashboard/tests/graphql/test_feedback.py b/server/vbv_lernwelt/dashboard/tests/graphql/test_feedback.py index 5ab9c9dd..0cb100e6 100644 --- a/server/vbv_lernwelt/dashboard/tests/graphql/test_feedback.py +++ b/server/vbv_lernwelt/dashboard/tests/graphql/test_feedback.py @@ -71,7 +71,7 @@ class DashboardFeedbackTestCase(GraphQLTestCase): query = f"""query($course_id: ID!) {{ course_statistics(course_id: $course_id) {{ - course_id + id feedback_responses {{ records {{ course_session_id diff --git a/server/vbv_lernwelt/iam/permissions.py b/server/vbv_lernwelt/iam/permissions.py index fb75507e..2c41e131 100644 --- a/server/vbv_lernwelt/iam/permissions.py +++ b/server/vbv_lernwelt/iam/permissions.py @@ -74,6 +74,14 @@ def can_view_course_session_group_statistics( return user in group.supervisor.all() +def can_view_course_session_progress(user: User, course_session: CourseSession) -> bool: + return CourseSessionUser.objects.filter( + course_session=course_session, + user=user, + role=CourseSessionUser.Role.MEMBER, + ).exists() + + def can_view_course_session(user: User, course_session: CourseSession) -> bool: if user.is_superuser: return True From adbd7c8c69880fb7b9caad9a8f7cd399b43d9c99 Mon Sep 17 00:00:00 2001 From: Livio Bieri Date: Wed, 25 Oct 2023 10:49:51 +0200 Subject: [PATCH 21/85] fix: more required=True in object types --- .../dashboard/graphql/types/attendance.py | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/server/vbv_lernwelt/dashboard/graphql/types/attendance.py b/server/vbv_lernwelt/dashboard/graphql/types/attendance.py index 01b41ac4..ac162f67 100644 --- a/server/vbv_lernwelt/dashboard/graphql/types/attendance.py +++ b/server/vbv_lernwelt/dashboard/graphql/types/attendance.py @@ -11,23 +11,23 @@ from vbv_lernwelt.notify.email.email_services import format_swiss_datetime class AttendanceSummary(graphene.ObjectType): - days_completed = graphene.Int() - participants_present = graphene.Int() + days_completed = graphene.Int(required=True) + participants_present = graphene.Int(required=True) class PresenceRecord(graphene.ObjectType): - course_session_id = graphene.ID() - generation = graphene.String() - circle_id = graphene.ID() - due_date = graphene.String() - participants_present = graphene.Int() - participants_total = graphene.Int() - cockpit_url = graphene.String() + course_session_id = graphene.ID(required=True) + generation = graphene.String(required=True) + circle_id = graphene.ID(required=True) + due_date = graphene.String(required=True) + participants_present = graphene.Int(required=True) + participants_total = graphene.Int(required=True) + cockpit_url = graphene.String(required=True) class AttendanceDayPresences(graphene.ObjectType): - records = graphene.List(PresenceRecord) - summary = graphene.Field(AttendanceSummary) + records = graphene.List(PresenceRecord, required=True) + summary = graphene.Field(AttendanceSummary, required=True) def attendance_day_presences( From 45c183e78b1bb25afedaeaced8aa619d5eb788a9 Mon Sep 17 00:00:00 2001 From: Livio Bieri Date: Wed, 25 Oct 2023 11:03:58 +0200 Subject: [PATCH 22/85] fix: dashboard config can be required --- server/vbv_lernwelt/dashboard/graphql/queries.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/vbv_lernwelt/dashboard/graphql/queries.py b/server/vbv_lernwelt/dashboard/graphql/queries.py index 59d102d6..b3584906 100644 --- a/server/vbv_lernwelt/dashboard/graphql/queries.py +++ b/server/vbv_lernwelt/dashboard/graphql/queries.py @@ -9,7 +9,7 @@ from vbv_lernwelt.iam.permissions import can_view_course_session_group_statistic class DashboardQuery(graphene.ObjectType): course_statistics = graphene.Field(CourseStatisticsType, course_id=graphene.ID(required=True)) - dashboard_config = graphene.List(DashboardConfigType, required=False) + dashboard_config = graphene.List(DashboardConfigType, required=True) def resolve_course_statistics(root, info, course_id: str): # noqa user = info.context.user From 6f973d7e933f1756af80893decf0c25c07ae0d67 Mon Sep 17 00:00:00 2001 From: Reto Aebersold Date: Wed, 25 Oct 2023 11:39:36 +0200 Subject: [PATCH 23/85] chore: format and generate gql schema / types --- client/src/gql/gql.ts | 5 + client/src/gql/graphql.ts | 165 ++++++++++++++++++ client/src/gql/schema.graphql | 150 +++++++++++++++- client/src/gql/typenames.ts | 20 +++ client/src/graphql/queries.ts | 10 ++ client/src/pages/DashPage.vue | 21 --- server/vbv_lernwelt/course/views.py | 12 +- .../vbv_lernwelt/dashboard/graphql/queries.py | 51 ++++-- .../tests/graphql/test_assignment.py | 4 +- .../tests/graphql/test_attendance.py | 4 +- .../tests/graphql/test_competence.py | 4 +- .../dashboard/tests/graphql/test_dashboard.py | 6 +- .../dashboard/tests/graphql/test_feedback.py | 4 +- .../tests/graphql/test_selection_metrics.py | 8 +- 14 files changed, 395 insertions(+), 69 deletions(-) delete mode 100644 client/src/pages/DashPage.vue diff --git a/client/src/gql/gql.ts b/client/src/gql/gql.ts index 554937b8..b6ef5e52 100644 --- a/client/src/gql/gql.ts +++ b/client/src/gql/gql.ts @@ -21,6 +21,7 @@ const documents = { "\n query competenceCertificateQuery($courseSlug: String!, $courseSessionId: ID!) {\n competence_certificate_list(course_slug: $courseSlug) {\n ...CoursePageFields\n competence_certificates {\n ...CoursePageFields\n assignments {\n ...CoursePageFields\n assignment_type\n max_points\n completion(course_session_id: $courseSessionId) {\n id\n completion_status\n submitted_at\n evaluation_points\n evaluation_max_points\n evaluation_passed\n }\n learning_content {\n ...CoursePageFields\n circle {\n id\n title\n slug\n }\n }\n }\n }\n }\n }\n": types.CompetenceCertificateQueryDocument, "\n query courseSessionDetail($courseSessionId: ID!) {\n course_session(id: $courseSessionId) {\n id\n title\n course {\n id\n title\n slug\n }\n users {\n id\n user_id\n first_name\n last_name\n email\n avatar_url\n role\n circles {\n id\n title\n slug\n }\n }\n attendance_courses {\n id\n location\n trainer\n due_date {\n id\n start\n end\n }\n learning_content_id\n learning_content {\n id\n title\n circle {\n id\n title\n slug\n }\n }\n }\n assignments {\n id\n submission_deadline {\n id\n start\n }\n evaluation_deadline {\n id\n start\n }\n learning_content {\n id\n title\n content_assignment {\n id\n title\n assignment_type\n }\n }\n }\n edoniq_tests {\n id\n deadline {\n id\n start\n end\n }\n learning_content {\n id\n title\n content_assignment {\n id\n title\n assignment_type\n }\n }\n }\n }\n }\n": types.CourseSessionDetailDocument, "\n query courseQuery($slug: String!) {\n course(slug: $slug) {\n id\n title\n slug\n category_name\n action_competences {\n competence_id\n ...CoursePageFields\n performance_criteria {\n competence_id\n learning_unit {\n id\n slug\n evaluate_url\n }\n ...CoursePageFields\n }\n }\n learning_path {\n ...CoursePageFields\n topics {\n is_visible\n ...CoursePageFields\n circles {\n description\n goals\n ...CoursePageFields\n learning_sequences {\n icon\n ...CoursePageFields\n learning_units {\n evaluate_url\n ...CoursePageFields\n performance_criteria {\n ...CoursePageFields\n }\n learning_contents {\n can_user_self_toggle_course_completion\n content_url\n minutes\n description\n ...CoursePageFields\n ... on LearningContentAssignmentObjectType {\n assignment_type\n content_assignment {\n id\n }\n competence_certificate {\n ...CoursePageFields\n }\n }\n ... on LearningContentEdoniqTestObjectType {\n checkbox_text\n has_extended_time_test\n content_assignment {\n id\n }\n competence_certificate {\n ...CoursePageFields\n }\n }\n ... on LearningContentRichTextObjectType {\n text\n }\n }\n }\n }\n }\n }\n }\n }\n }\n": types.CourseQueryDocument, + "\n query dashboardConfig {\n dashboard_config {\n id\n name\n dashboard_type\n }\n }\n": types.DashboardConfigDocument, "\n mutation SendFeedbackMutation(\n $courseSessionId: ID!\n $learningContentId: ID!\n $data: GenericScalar!\n $submitted: Boolean\n ) {\n send_feedback(\n course_session_id: $courseSessionId\n learning_content_page_id: $learningContentId\n data: $data\n submitted: $submitted\n ) {\n feedback_response {\n id\n data\n submitted\n }\n errors {\n field\n messages\n }\n }\n }\n": types.SendFeedbackMutationDocument, }; @@ -70,6 +71,10 @@ export function graphql(source: "\n query courseSessionDetail($courseSessionId: * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ export function graphql(source: "\n query courseQuery($slug: String!) {\n course(slug: $slug) {\n id\n title\n slug\n category_name\n action_competences {\n competence_id\n ...CoursePageFields\n performance_criteria {\n competence_id\n learning_unit {\n id\n slug\n evaluate_url\n }\n ...CoursePageFields\n }\n }\n learning_path {\n ...CoursePageFields\n topics {\n is_visible\n ...CoursePageFields\n circles {\n description\n goals\n ...CoursePageFields\n learning_sequences {\n icon\n ...CoursePageFields\n learning_units {\n evaluate_url\n ...CoursePageFields\n performance_criteria {\n ...CoursePageFields\n }\n learning_contents {\n can_user_self_toggle_course_completion\n content_url\n minutes\n description\n ...CoursePageFields\n ... on LearningContentAssignmentObjectType {\n assignment_type\n content_assignment {\n id\n }\n competence_certificate {\n ...CoursePageFields\n }\n }\n ... on LearningContentEdoniqTestObjectType {\n checkbox_text\n has_extended_time_test\n content_assignment {\n id\n }\n competence_certificate {\n ...CoursePageFields\n }\n }\n ... on LearningContentRichTextObjectType {\n text\n }\n }\n }\n }\n }\n }\n }\n }\n }\n"): (typeof documents)["\n query courseQuery($slug: String!) {\n course(slug: $slug) {\n id\n title\n slug\n category_name\n action_competences {\n competence_id\n ...CoursePageFields\n performance_criteria {\n competence_id\n learning_unit {\n id\n slug\n evaluate_url\n }\n ...CoursePageFields\n }\n }\n learning_path {\n ...CoursePageFields\n topics {\n is_visible\n ...CoursePageFields\n circles {\n description\n goals\n ...CoursePageFields\n learning_sequences {\n icon\n ...CoursePageFields\n learning_units {\n evaluate_url\n ...CoursePageFields\n performance_criteria {\n ...CoursePageFields\n }\n learning_contents {\n can_user_self_toggle_course_completion\n content_url\n minutes\n description\n ...CoursePageFields\n ... on LearningContentAssignmentObjectType {\n assignment_type\n content_assignment {\n id\n }\n competence_certificate {\n ...CoursePageFields\n }\n }\n ... on LearningContentEdoniqTestObjectType {\n checkbox_text\n has_extended_time_test\n content_assignment {\n id\n }\n competence_certificate {\n ...CoursePageFields\n }\n }\n ... on LearningContentRichTextObjectType {\n text\n }\n }\n }\n }\n }\n }\n }\n }\n }\n"]; +/** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function graphql(source: "\n query dashboardConfig {\n dashboard_config {\n id\n name\n dashboard_type\n }\n }\n"): (typeof documents)["\n query dashboardConfig {\n dashboard_config {\n id\n name\n dashboard_type\n }\n }\n"]; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ diff --git a/client/src/gql/graphql.ts b/client/src/gql/graphql.ts index d2ac2e1c..af755314 100644 --- a/client/src/gql/graphql.ts +++ b/client/src/gql/graphql.ts @@ -85,6 +85,15 @@ export type AssignmentAssignmentCompletionCompletionStatusChoices = /** SUBMITTED */ | 'SUBMITTED'; +export type AssignmentCompletionMetrics = { + __typename?: 'AssignmentCompletionMetrics'; + average_passed: Scalars['Float']['output']; + failed_count: Scalars['Int']['output']; + passed_count: Scalars['Int']['output']; + ranking_completed: Scalars['Boolean']['output']; + unranked_count: Scalars['Int']['output']; +}; + export type AssignmentCompletionMutation = { __typename?: 'AssignmentCompletionMutation'; assignment_completion?: Maybe; @@ -156,11 +165,48 @@ export type AssignmentObjectTypeCompletionArgs = { learning_content_page_id?: InputMaybe; }; +export type AssignmentRecord = { + __typename?: 'AssignmentRecord'; + assignment_title: Scalars['String']['output']; + assignment_type_translation_key: Scalars['String']['output']; + circle_id: Scalars['ID']['output']; + course_session_assignment_id: Scalars['ID']['output']; + course_session_id: Scalars['ID']['output']; + deadline: Scalars['DateTime']['output']; + details_url: Scalars['String']['output']; + generation: Scalars['String']['output']; + metrics: AssignmentCompletionMetrics; +}; + +export type AssignmentSummary = { + __typename?: 'AssignmentSummary'; + average_passed: Scalars['Float']['output']; + completed_count: Scalars['Int']['output']; +}; + +export type Assignments = { + __typename?: 'Assignments'; + records: Array>; + summary: AssignmentSummary; +}; + export type AttendanceCourseUserMutation = { __typename?: 'AttendanceCourseUserMutation'; course_session_attendance_course?: Maybe; }; +export type AttendanceDayPresences = { + __typename?: 'AttendanceDayPresences'; + records: Array>; + summary: AttendanceSummary; +}; + +export type AttendanceSummary = { + __typename?: 'AttendanceSummary'; + days_completed: Scalars['Int']['output']; + participants_present: Scalars['Int']['output']; +}; + export type AttendanceUserInputType = { status: AttendanceUserStatus; user_id: Scalars['UUID']['input']; @@ -180,6 +226,13 @@ export type AttendanceUserStatus = | 'ABSENT' | 'PRESENT'; +export type CircleData = { + __typename?: 'CircleData'; + circle_id: Scalars['ID']['output']; + circle_title: Scalars['String']['output']; + experts: Array>; +}; + export type CircleLightObjectType = { __typename?: 'CircleLightObjectType'; id: Scalars['ID']['output']; @@ -228,6 +281,27 @@ export type CompetenceCertificateObjectType = CoursePageInterface & { translation_key: Scalars['String']['output']; }; +export type CompetencePerformance = { + __typename?: 'CompetencePerformance'; + circle_id: Scalars['ID']['output']; + course_session_id: Scalars['ID']['output']; + fail_count: Scalars['Int']['output']; + generation: Scalars['String']['output']; + success_count: Scalars['Int']['output']; +}; + +export type Competences = { + __typename?: 'Competences'; + performances: Array>; + summary: CompletionSummary; +}; + +export type CompletionSummary = { + __typename?: 'CompletionSummary'; + fail_total: Scalars['Int']['output']; + success_total: Scalars['Int']['output']; +}; + /** An enumeration. */ export type CoreUserLanguageChoices = /** Deutsch */ @@ -289,6 +363,12 @@ export type CourseSessionAttendanceCourseObjectType = { trainer: Scalars['String']['output']; }; +export type CourseSessionData = { + __typename?: 'CourseSessionData'; + session_id: Scalars['ID']['output']; + session_title: Scalars['String']['output']; +}; + export type CourseSessionEdoniqTestObjectType = { __typename?: 'CourseSessionEdoniqTestObjectType'; course_session_id: Scalars['ID']['output']; @@ -313,6 +393,13 @@ export type CourseSessionObjectType = { users: Array; }; +export type CourseSessionProperties = { + __typename?: 'CourseSessionProperties'; + circles: Array>; + generations: Array>; + sessions: Array>; +}; + export type CourseSessionUserExpertCircleType = { __typename?: 'CourseSessionUserExpertCircleType'; id: Scalars['ID']['output']; @@ -332,6 +419,38 @@ export type CourseSessionUserObjectsType = { user_id: Scalars['UUID']['output']; }; +export type CourseSessionsSelectionMetrics = { + __typename?: 'CourseSessionsSelectionMetrics'; + expert_count: Scalars['Int']['output']; + participant_count: Scalars['Int']['output']; + session_count: Scalars['Int']['output']; +}; + +export type CourseStatisticsType = { + __typename?: 'CourseStatisticsType'; + assignments: Assignments; + attendance_day_presences: AttendanceDayPresences; + competences: Competences; + course_session_properties: CourseSessionProperties; + course_session_selection_ids: Array>; + course_session_selection_metrics: CourseSessionsSelectionMetrics; + course_title: Scalars['String']['output']; + feedback_responses: FeedbackResponses; + id: Scalars['ID']['output']; +}; + +export type DashboardConfigType = { + __typename?: 'DashboardConfigType'; + dashboard_type: DashboardType; + id: Scalars['ID']['output']; + name: Scalars['String']['output']; +}; + +export type DashboardType = + | 'PROGRESS_DASHBOARD' + | 'SIMPLE_LIST_DASHBOARD' + | 'STATISTICS_DASHBOARD'; + export type DueDateObjectType = { __typename?: 'DueDateObjectType'; /** Translation Key aus dem Frontend */ @@ -360,6 +479,15 @@ export type ErrorType = { messages: Array; }; +export type FeedbackRecord = { + __typename?: 'FeedbackRecord'; + circle_id: Scalars['ID']['output']; + course_session_id: Scalars['ID']['output']; + generation: Scalars['String']['output']; + satisfaction_average: Scalars['Float']['output']; + satisfaction_max: Scalars['Int']['output']; +}; + export type FeedbackResponseObjectType = { __typename?: 'FeedbackResponseObjectType'; data?: Maybe; @@ -367,6 +495,19 @@ export type FeedbackResponseObjectType = { submitted: Scalars['Boolean']['output']; }; +export type FeedbackResponses = { + __typename?: 'FeedbackResponses'; + records: Array>; + summary: FeedbackSummary; +}; + +export type FeedbackSummary = { + __typename?: 'FeedbackSummary'; + satisfaction_average: Scalars['Float']['output']; + satisfaction_max: Scalars['Int']['output']; + total_responses: Scalars['Int']['output']; +}; + export type LearningContentAssignmentObjectType = CoursePageInterface & LearningContentInterface & { __typename?: 'LearningContentAssignmentObjectType'; assignment_type: LearnpathLearningContentAssignmentAssignmentTypeChoices; @@ -665,6 +806,17 @@ export type PerformanceCriteriaObjectType = CoursePageInterface & { translation_key: Scalars['String']['output']; }; +export type PresenceRecord = { + __typename?: 'PresenceRecord'; + circle_id: Scalars['ID']['output']; + cockpit_url: Scalars['String']['output']; + course_session_id: Scalars['ID']['output']; + due_date: Scalars['String']['output']; + generation: Scalars['String']['output']; + participants_present: Scalars['Int']['output']; + participants_total: Scalars['Int']['output']; +}; + export type Query = { __typename?: 'Query'; assignment?: Maybe; @@ -674,6 +826,8 @@ export type Query = { course?: Maybe; course_session?: Maybe; course_session_attendance_course?: Maybe; + course_statistics?: Maybe; + dashboard_config: Array; learning_content_assignment?: Maybe; learning_content_attendance_course?: Maybe; learning_content_document_list?: Maybe; @@ -733,6 +887,11 @@ export type QueryCourseSessionAttendanceCourseArgs = { }; +export type QueryCourseStatisticsArgs = { + course_id: Scalars['ID']['input']; +}; + + export type QueryLearningPathArgs = { course_id?: InputMaybe; course_slug?: InputMaybe; @@ -978,6 +1137,11 @@ export type CourseQueryQuery = { __typename?: 'Query', course?: { __typename?: ' & { ' $fragmentRefs'?: { 'CoursePageFieldsLearningPathObjectTypeFragment': CoursePageFieldsLearningPathObjectTypeFragment } } ) } | null }; +export type DashboardConfigQueryVariables = Exact<{ [key: string]: never; }>; + + +export type DashboardConfigQuery = { __typename?: 'Query', dashboard_config: Array<{ __typename?: 'DashboardConfigType', id: string, name: string, dashboard_type: DashboardType }> }; + export type SendFeedbackMutationMutationVariables = Exact<{ courseSessionId: Scalars['ID']['input']; learningContentId: Scalars['ID']['input']; @@ -996,4 +1160,5 @@ export const AssignmentCompletionQueryDocument = {"kind":"Document","definitions export const CompetenceCertificateQueryDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"competenceCertificateQuery"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"courseSlug"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"courseSessionId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"competence_certificate_list"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"course_slug"},"value":{"kind":"Variable","name":{"kind":"Name","value":"courseSlug"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"CoursePageFields"}},{"kind":"Field","name":{"kind":"Name","value":"competence_certificates"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"CoursePageFields"}},{"kind":"Field","name":{"kind":"Name","value":"assignments"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"CoursePageFields"}},{"kind":"Field","name":{"kind":"Name","value":"assignment_type"}},{"kind":"Field","name":{"kind":"Name","value":"max_points"}},{"kind":"Field","name":{"kind":"Name","value":"completion"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"course_session_id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"courseSessionId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"completion_status"}},{"kind":"Field","name":{"kind":"Name","value":"submitted_at"}},{"kind":"Field","name":{"kind":"Name","value":"evaluation_points"}},{"kind":"Field","name":{"kind":"Name","value":"evaluation_max_points"}},{"kind":"Field","name":{"kind":"Name","value":"evaluation_passed"}}]}},{"kind":"Field","name":{"kind":"Name","value":"learning_content"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"CoursePageFields"}},{"kind":"Field","name":{"kind":"Name","value":"circle"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}}]}}]}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"CoursePageFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"CoursePageInterface"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"content_type"}},{"kind":"Field","name":{"kind":"Name","value":"frontend_url"}}]}}]} as unknown as DocumentNode; export const CourseSessionDetailDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"courseSessionDetail"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"courseSessionId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"course_session"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"courseSessionId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"course"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}}]}},{"kind":"Field","name":{"kind":"Name","value":"users"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"user_id"}},{"kind":"Field","name":{"kind":"Name","value":"first_name"}},{"kind":"Field","name":{"kind":"Name","value":"last_name"}},{"kind":"Field","name":{"kind":"Name","value":"email"}},{"kind":"Field","name":{"kind":"Name","value":"avatar_url"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"circles"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"attendance_courses"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"location"}},{"kind":"Field","name":{"kind":"Name","value":"trainer"}},{"kind":"Field","name":{"kind":"Name","value":"due_date"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"start"}},{"kind":"Field","name":{"kind":"Name","value":"end"}}]}},{"kind":"Field","name":{"kind":"Name","value":"learning_content_id"}},{"kind":"Field","name":{"kind":"Name","value":"learning_content"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"circle"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"assignments"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"submission_deadline"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"start"}}]}},{"kind":"Field","name":{"kind":"Name","value":"evaluation_deadline"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"start"}}]}},{"kind":"Field","name":{"kind":"Name","value":"learning_content"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"content_assignment"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"assignment_type"}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"edoniq_tests"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"deadline"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"start"}},{"kind":"Field","name":{"kind":"Name","value":"end"}}]}},{"kind":"Field","name":{"kind":"Name","value":"learning_content"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"content_assignment"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"assignment_type"}}]}}]}}]}}]}}]}}]} as unknown as DocumentNode; export const CourseQueryDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"courseQuery"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"slug"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"course"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"slug"},"value":{"kind":"Variable","name":{"kind":"Name","value":"slug"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"category_name"}},{"kind":"Field","name":{"kind":"Name","value":"action_competences"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"competence_id"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"CoursePageFields"}},{"kind":"Field","name":{"kind":"Name","value":"performance_criteria"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"competence_id"}},{"kind":"Field","name":{"kind":"Name","value":"learning_unit"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"evaluate_url"}}]}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"CoursePageFields"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"learning_path"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"CoursePageFields"}},{"kind":"Field","name":{"kind":"Name","value":"topics"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"is_visible"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"CoursePageFields"}},{"kind":"Field","name":{"kind":"Name","value":"circles"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"goals"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"CoursePageFields"}},{"kind":"Field","name":{"kind":"Name","value":"learning_sequences"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"icon"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"CoursePageFields"}},{"kind":"Field","name":{"kind":"Name","value":"learning_units"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"evaluate_url"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"CoursePageFields"}},{"kind":"Field","name":{"kind":"Name","value":"performance_criteria"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"CoursePageFields"}}]}},{"kind":"Field","name":{"kind":"Name","value":"learning_contents"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"can_user_self_toggle_course_completion"}},{"kind":"Field","name":{"kind":"Name","value":"content_url"}},{"kind":"Field","name":{"kind":"Name","value":"minutes"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"CoursePageFields"}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"LearningContentAssignmentObjectType"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"assignment_type"}},{"kind":"Field","name":{"kind":"Name","value":"content_assignment"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"Field","name":{"kind":"Name","value":"competence_certificate"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"CoursePageFields"}}]}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"LearningContentEdoniqTestObjectType"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"checkbox_text"}},{"kind":"Field","name":{"kind":"Name","value":"has_extended_time_test"}},{"kind":"Field","name":{"kind":"Name","value":"content_assignment"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"Field","name":{"kind":"Name","value":"competence_certificate"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"CoursePageFields"}}]}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"LearningContentRichTextObjectType"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"text"}}]}}]}}]}}]}}]}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"CoursePageFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"CoursePageInterface"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"content_type"}},{"kind":"Field","name":{"kind":"Name","value":"frontend_url"}}]}}]} as unknown as DocumentNode; +export const DashboardConfigDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"dashboardConfig"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"dashboard_config"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"dashboard_type"}}]}}]}}]} as unknown as DocumentNode; export const SendFeedbackMutationDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"SendFeedbackMutation"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"courseSessionId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"learningContentId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"data"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"GenericScalar"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"submitted"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Boolean"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"send_feedback"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"course_session_id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"courseSessionId"}}},{"kind":"Argument","name":{"kind":"Name","value":"learning_content_page_id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"learningContentId"}}},{"kind":"Argument","name":{"kind":"Name","value":"data"},"value":{"kind":"Variable","name":{"kind":"Name","value":"data"}}},{"kind":"Argument","name":{"kind":"Name","value":"submitted"},"value":{"kind":"Variable","name":{"kind":"Name","value":"submitted"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"feedback_response"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"data"}},{"kind":"Field","name":{"kind":"Name","value":"submitted"}}]}},{"kind":"Field","name":{"kind":"Name","value":"errors"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"field"}},{"kind":"Field","name":{"kind":"Name","value":"messages"}}]}}]}}]}}]} as unknown as DocumentNode; \ No newline at end of file diff --git a/client/src/gql/schema.graphql b/client/src/gql/schema.graphql index d443746f..e50c8055 100644 --- a/client/src/gql/schema.graphql +++ b/client/src/gql/schema.graphql @@ -1,4 +1,6 @@ type Query { + course_statistics(course_id: ID!): CourseStatisticsType + dashboard_config: [DashboardConfigType!]! learning_path(id: ID, slug: String, course_id: ID, course_slug: String): LearningPathObjectType course_session_attendance_course(id: ID!, assignment_user_id: ID): CourseSessionAttendanceCourseObjectType course(id: ID, slug: String): CourseObjectType @@ -19,6 +21,147 @@ type Query { assignment_completion(assignment_id: ID!, course_session_id: ID!, learning_content_page_id: ID, assignment_user_id: UUID): AssignmentCompletionObjectType } +type CourseStatisticsType { + id: ID! + course_title: String! + course_session_properties: CourseSessionProperties! + course_session_selection_ids: [ID]! + course_session_selection_metrics: CourseSessionsSelectionMetrics! + attendance_day_presences: AttendanceDayPresences! + feedback_responses: FeedbackResponses! + assignments: Assignments! + competences: Competences! +} + +type CourseSessionProperties { + sessions: [CourseSessionData]! + generations: [String]! + circles: [CircleData]! +} + +type CourseSessionData { + session_id: ID! + session_title: String! +} + +type CircleData { + circle_id: ID! + circle_title: String! + experts: [String]! +} + +type CourseSessionsSelectionMetrics { + session_count: Int! + participant_count: Int! + expert_count: Int! +} + +type AttendanceDayPresences { + records: [PresenceRecord]! + summary: AttendanceSummary! +} + +type PresenceRecord { + course_session_id: ID! + generation: String! + circle_id: ID! + due_date: String! + participants_present: Int! + participants_total: Int! + cockpit_url: String! +} + +type AttendanceSummary { + days_completed: Int! + participants_present: Int! +} + +type FeedbackResponses { + records: [FeedbackRecord]! + summary: FeedbackSummary! +} + +type FeedbackRecord { + course_session_id: ID! + generation: String! + circle_id: ID! + satisfaction_average: Float! + satisfaction_max: Int! +} + +type FeedbackSummary { + satisfaction_average: Float! + satisfaction_max: Int! + total_responses: Int! +} + +type Assignments { + records: [AssignmentRecord]! + summary: AssignmentSummary! +} + +type AssignmentRecord { + course_session_id: ID! + course_session_assignment_id: ID! + circle_id: ID! + generation: String! + assignment_type_translation_key: String! + assignment_title: String! + deadline: DateTime! + metrics: AssignmentCompletionMetrics! + details_url: String! +} + +""" +The `DateTime` scalar type represents a DateTime +value as specified by +[iso8601](https://en.wikipedia.org/wiki/ISO_8601). +""" +scalar DateTime + +type AssignmentCompletionMetrics { + passed_count: Int! + failed_count: Int! + unranked_count: Int! + ranking_completed: Boolean! + average_passed: Float! +} + +type AssignmentSummary { + completed_count: Int! + average_passed: Float! +} + +type Competences { + performances: [CompetencePerformance]! + summary: CompletionSummary! +} + +type CompetencePerformance { + course_session_id: ID! + generation: String! + circle_id: ID! + success_count: Int! + fail_count: Int! +} + +type CompletionSummary { + success_total: Int! + fail_total: Int! +} + +type DashboardConfigType { + id: ID! + name: String! + dashboard_type: DashboardType! +} + +enum DashboardType { + STATISTICS_DASHBOARD + PROGRESS_DASHBOARD + SIMPLE_LIST_DASHBOARD +} + type LearningPathObjectType implements CoursePageInterface { id: ID! title: String! @@ -214,13 +357,6 @@ type DueDateObjectType { course_session: CourseSessionObjectType! } -""" -The `DateTime` scalar type represents a DateTime -value as specified by -[iso8601](https://en.wikipedia.org/wiki/ISO_8601). -""" -scalar DateTime - type CourseSessionObjectType { id: ID! created_at: DateTime! diff --git a/client/src/gql/typenames.ts b/client/src/gql/typenames.ts index 2302eac2..c870470a 100644 --- a/client/src/gql/typenames.ts +++ b/client/src/gql/typenames.ts @@ -1,34 +1,53 @@ export const ActionCompetenceObjectType = "ActionCompetenceObjectType"; export const AssignmentAssignmentAssignmentTypeChoices = "AssignmentAssignmentAssignmentTypeChoices"; export const AssignmentAssignmentCompletionCompletionStatusChoices = "AssignmentAssignmentCompletionCompletionStatusChoices"; +export const AssignmentCompletionMetrics = "AssignmentCompletionMetrics"; export const AssignmentCompletionMutation = "AssignmentCompletionMutation"; export const AssignmentCompletionObjectType = "AssignmentCompletionObjectType"; export const AssignmentCompletionStatus = "AssignmentCompletionStatus"; export const AssignmentObjectType = "AssignmentObjectType"; +export const AssignmentRecord = "AssignmentRecord"; +export const AssignmentSummary = "AssignmentSummary"; +export const Assignments = "Assignments"; export const AttendanceCourseUserMutation = "AttendanceCourseUserMutation"; +export const AttendanceDayPresences = "AttendanceDayPresences"; +export const AttendanceSummary = "AttendanceSummary"; export const AttendanceUserInputType = "AttendanceUserInputType"; export const AttendanceUserObjectType = "AttendanceUserObjectType"; export const AttendanceUserStatus = "AttendanceUserStatus"; export const Boolean = "Boolean"; +export const CircleData = "CircleData"; export const CircleLightObjectType = "CircleLightObjectType"; export const CircleObjectType = "CircleObjectType"; export const CompetenceCertificateListObjectType = "CompetenceCertificateListObjectType"; export const CompetenceCertificateObjectType = "CompetenceCertificateObjectType"; +export const CompetencePerformance = "CompetencePerformance"; +export const Competences = "Competences"; +export const CompletionSummary = "CompletionSummary"; export const CoreUserLanguageChoices = "CoreUserLanguageChoices"; export const CourseCourseSessionUserRoleChoices = "CourseCourseSessionUserRoleChoices"; export const CourseObjectType = "CourseObjectType"; export const CoursePageInterface = "CoursePageInterface"; export const CourseSessionAssignmentObjectType = "CourseSessionAssignmentObjectType"; export const CourseSessionAttendanceCourseObjectType = "CourseSessionAttendanceCourseObjectType"; +export const CourseSessionData = "CourseSessionData"; export const CourseSessionEdoniqTestObjectType = "CourseSessionEdoniqTestObjectType"; export const CourseSessionObjectType = "CourseSessionObjectType"; +export const CourseSessionProperties = "CourseSessionProperties"; export const CourseSessionUserExpertCircleType = "CourseSessionUserExpertCircleType"; export const CourseSessionUserObjectsType = "CourseSessionUserObjectsType"; +export const CourseSessionsSelectionMetrics = "CourseSessionsSelectionMetrics"; +export const CourseStatisticsType = "CourseStatisticsType"; +export const DashboardConfigType = "DashboardConfigType"; +export const DashboardType = "DashboardType"; export const Date = "Date"; export const DateTime = "DateTime"; export const DueDateObjectType = "DueDateObjectType"; export const ErrorType = "ErrorType"; +export const FeedbackRecord = "FeedbackRecord"; export const FeedbackResponseObjectType = "FeedbackResponseObjectType"; +export const FeedbackResponses = "FeedbackResponses"; +export const FeedbackSummary = "FeedbackSummary"; export const Float = "Float"; export const GenericScalar = "GenericScalar"; export const ID = "ID"; @@ -52,6 +71,7 @@ export const LearningUnitObjectType = "LearningUnitObjectType"; export const LearnpathLearningContentAssignmentAssignmentTypeChoices = "LearnpathLearningContentAssignmentAssignmentTypeChoices"; export const Mutation = "Mutation"; export const PerformanceCriteriaObjectType = "PerformanceCriteriaObjectType"; +export const PresenceRecord = "PresenceRecord"; export const Query = "Query"; export const SendFeedbackMutation = "SendFeedbackMutation"; export const String = "String"; diff --git a/client/src/graphql/queries.ts b/client/src/graphql/queries.ts index d995e4f3..7751e093 100644 --- a/client/src/graphql/queries.ts +++ b/client/src/graphql/queries.ts @@ -268,3 +268,13 @@ export const COURSE_QUERY = graphql(` } } `); + +export const DASHBOARD_CONFIG = graphql(` + query dashboardConfig { + dashboard_config { + id + name + dashboard_type + } + } +`); diff --git a/client/src/pages/DashPage.vue b/client/src/pages/DashPage.vue deleted file mode 100644 index eac1f744..00000000 --- a/client/src/pages/DashPage.vue +++ /dev/null @@ -1,21 +0,0 @@ - - - - - diff --git a/server/vbv_lernwelt/course/views.py b/server/vbv_lernwelt/course/views.py index 627ab7a8..fa8e4686 100644 --- a/server/vbv_lernwelt/course/views.py +++ b/server/vbv_lernwelt/course/views.py @@ -6,11 +6,7 @@ from rest_framework.response import Response from wagtail.models import Page from vbv_lernwelt.core.utils import get_django_content_type -from vbv_lernwelt.course.models import ( - CircleDocument, - CourseCompletion, - CourseSession, -) +from vbv_lernwelt.course.models import CircleDocument, CourseCompletion, CourseSession from vbv_lernwelt.course.serializers import ( CourseCompletionSerializer, CourseSessionSerializer, @@ -21,11 +17,11 @@ from vbv_lernwelt.course.services import mark_course_completion from vbv_lernwelt.files.models import UploadFile from vbv_lernwelt.files.services import FileDirectUploadService from vbv_lernwelt.iam.permissions import ( - has_course_access_by_page_request, - has_course_access, - is_course_session_expert, course_sessions_for_user_qs, + has_course_access, + has_course_access_by_page_request, is_circle_expert, + is_course_session_expert, ) logger = structlog.get_logger(__name__) diff --git a/server/vbv_lernwelt/dashboard/graphql/queries.py b/server/vbv_lernwelt/dashboard/graphql/queries.py index b3584906..797c0b4c 100644 --- a/server/vbv_lernwelt/dashboard/graphql/queries.py +++ b/server/vbv_lernwelt/dashboard/graphql/queries.py @@ -1,17 +1,28 @@ import graphene -from vbv_lernwelt.course.models import CourseSession, Course +from vbv_lernwelt.course.models import Course, CourseSession from vbv_lernwelt.course_session_group.models import CourseSessionGroup -from vbv_lernwelt.dashboard.graphql.types.dashboard import CourseStatisticsType, DashboardConfigType, DashboardType -from vbv_lernwelt.iam.permissions import can_view_course_session_group_statistics, can_view_course_session, \ - can_view_course_session_progress +from vbv_lernwelt.dashboard.graphql.types.dashboard import ( + CourseStatisticsType, + DashboardConfigType, + DashboardType, +) +from vbv_lernwelt.iam.permissions import ( + can_view_course_session, + can_view_course_session_group_statistics, + can_view_course_session_progress, +) class DashboardQuery(graphene.ObjectType): - course_statistics = graphene.Field(CourseStatisticsType, course_id=graphene.ID(required=True)) - dashboard_config = graphene.List(DashboardConfigType, required=True) + course_statistics = graphene.Field( + CourseStatisticsType, course_id=graphene.ID(required=True) + ) + dashboard_config = graphene.List( + graphene.NonNull(DashboardConfigType), required=True + ) - def resolve_course_statistics(root, info, course_id: str): # noqa + def resolve_course_statistics(root, info, course_id: str): # noqa user = info.context.user course = Course.objects.get(id=course_id) @@ -19,18 +30,20 @@ class DashboardQuery(graphene.ObjectType): for group in CourseSessionGroup.objects.filter(course=course): if can_view_course_session_group_statistics(user=user, group=group): - course_session_ids.update(group.course_session.all().values_list("id", flat=True)) + course_session_ids.update( + group.course_session.all().values_list("id", flat=True) + ) if not course_session_ids: return None + return CourseStatisticsType( + id=course.id, + course_title=course.title, # noqa + course_session_selection_ids=list(course_session_ids), + ) # noqa - return CourseStatisticsType(id=course.id, course_title=course.title, # noqa - course_session_selection_ids=list(course_session_ids)) # noqa - - - - def resolve_dashboard_config(root, info): # noqa + def resolve_dashboard_config(root, info): # noqa user = info.context.user course_index = set() @@ -44,7 +57,7 @@ class DashboardQuery(graphene.ObjectType): { "id": str(course.id), "name": course.title, - "dashboard_type": DashboardType.STATISTICS_DASHBOARD + "dashboard_type": DashboardType.STATISTICS_DASHBOARD, } ) @@ -55,15 +68,17 @@ class DashboardQuery(graphene.ObjectType): { "id": str(course.id), "name": course.title, - "dashboard_type": DashboardType.SIMPLE_LIST_DASHBOARD + "dashboard_type": DashboardType.SIMPLE_LIST_DASHBOARD, } ) - if can_view_course_session_progress(user=user, course_session=course_session): + if can_view_course_session_progress( + user=user, course_session=course_session + ): dashboards.append( { "id": str(course.id), "name": course.title, - "dashboard_type": DashboardType.PROGRESS_DASHBOARD + "dashboard_type": DashboardType.PROGRESS_DASHBOARD, } ) diff --git a/server/vbv_lernwelt/dashboard/tests/graphql/test_assignment.py b/server/vbv_lernwelt/dashboard/tests/graphql/test_assignment.py index 1dede516..734129d4 100644 --- a/server/vbv_lernwelt/dashboard/tests/graphql/test_assignment.py +++ b/server/vbv_lernwelt/dashboard/tests/graphql/test_assignment.py @@ -10,6 +10,7 @@ from vbv_lernwelt.course_session.models import ( CourseSessionEdoniqTest, ) from vbv_lernwelt.dashboard.tests.graphql.utils import ( + add_course_session_group_supervisor, add_course_session_user, create_assignment, create_assignment_completion, @@ -19,9 +20,8 @@ from vbv_lernwelt.dashboard.tests.graphql.utils import ( create_course_session, create_course_session_assignment, create_course_session_edoniq_test, - create_user, create_course_session_group, - add_course_session_group_supervisor, + create_user, ) from vbv_lernwelt.learnpath.models import Circle diff --git a/server/vbv_lernwelt/dashboard/tests/graphql/test_attendance.py b/server/vbv_lernwelt/dashboard/tests/graphql/test_attendance.py index 17fce53a..29734f4e 100644 --- a/server/vbv_lernwelt/dashboard/tests/graphql/test_attendance.py +++ b/server/vbv_lernwelt/dashboard/tests/graphql/test_attendance.py @@ -6,14 +6,14 @@ from graphene_django.utils import GraphQLTestCase from vbv_lernwelt.course.models import CourseSessionUser from vbv_lernwelt.course_session.services.attendance import AttendanceUserStatus from vbv_lernwelt.dashboard.tests.graphql.utils import ( + add_course_session_group_supervisor, add_course_session_user, create_attendance_course, create_circle, create_course, create_course_session, - create_user, create_course_session_group, - add_course_session_group_supervisor, + create_user, ) diff --git a/server/vbv_lernwelt/dashboard/tests/graphql/test_competence.py b/server/vbv_lernwelt/dashboard/tests/graphql/test_competence.py index f2ce8f03..88658e6a 100644 --- a/server/vbv_lernwelt/dashboard/tests/graphql/test_competence.py +++ b/server/vbv_lernwelt/dashboard/tests/graphql/test_competence.py @@ -3,14 +3,14 @@ from graphene_django.utils import GraphQLTestCase from vbv_lernwelt.course.models import CourseSessionUser from vbv_lernwelt.course.services import mark_course_completion from vbv_lernwelt.dashboard.tests.graphql.utils import ( + add_course_session_group_supervisor, add_course_session_user, create_circle, create_course, create_course_session, + create_course_session_group, create_performance_criteria_page, create_user, - create_course_session_group, - add_course_session_group_supervisor, ) diff --git a/server/vbv_lernwelt/dashboard/tests/graphql/test_dashboard.py b/server/vbv_lernwelt/dashboard/tests/graphql/test_dashboard.py index ceb28fd7..e7b4de92 100644 --- a/server/vbv_lernwelt/dashboard/tests/graphql/test_dashboard.py +++ b/server/vbv_lernwelt/dashboard/tests/graphql/test_dashboard.py @@ -2,12 +2,12 @@ from graphene_django.utils import GraphQLTestCase from vbv_lernwelt.course.models import CourseSessionUser from vbv_lernwelt.dashboard.tests.graphql.utils import ( + add_course_session_group_supervisor, + add_course_session_user, create_course, create_course_session, - create_user, - add_course_session_user, create_course_session_group, - add_course_session_group_supervisor, + create_user, ) diff --git a/server/vbv_lernwelt/dashboard/tests/graphql/test_feedback.py b/server/vbv_lernwelt/dashboard/tests/graphql/test_feedback.py index 0cb100e6..a7e5c2fa 100644 --- a/server/vbv_lernwelt/dashboard/tests/graphql/test_feedback.py +++ b/server/vbv_lernwelt/dashboard/tests/graphql/test_feedback.py @@ -2,13 +2,13 @@ from graphene_django.utils import GraphQLTestCase from vbv_lernwelt.course.models import CourseSessionUser from vbv_lernwelt.dashboard.tests.graphql.utils import ( + add_course_session_group_supervisor, add_course_session_user, create_circle, create_course, create_course_session, - create_user, create_course_session_group, - add_course_session_group_supervisor, + create_user, ) from vbv_lernwelt.feedback.models import FeedbackResponse diff --git a/server/vbv_lernwelt/dashboard/tests/graphql/test_selection_metrics.py b/server/vbv_lernwelt/dashboard/tests/graphql/test_selection_metrics.py index bd2665da..c290aec5 100644 --- a/server/vbv_lernwelt/dashboard/tests/graphql/test_selection_metrics.py +++ b/server/vbv_lernwelt/dashboard/tests/graphql/test_selection_metrics.py @@ -2,13 +2,13 @@ from graphene_django.utils import GraphQLTestCase from vbv_lernwelt.course.models import CourseSessionUser from vbv_lernwelt.dashboard.tests.graphql.utils import ( + add_course_session_group_course_session, + add_course_session_group_supervisor, + add_course_session_user, create_course, create_course_session, - create_user, - add_course_session_user, create_course_session_group, - add_course_session_group_supervisor, - add_course_session_group_course_session, + create_user, ) From 04d40e1f57b4e3703e89dc152942e3e5f497fdf4 Mon Sep 17 00:00:00 2001 From: Livio Bieri Date: Wed, 25 Oct 2023 12:46:58 +0200 Subject: [PATCH 24/85] fix: dedup course dashboard stuff --- .../vbv_lernwelt/dashboard/graphql/queries.py | 117 ++++++++++++------ .../dashboard/tests/graphql/test_dashboard.py | 32 ++++- 2 files changed, 106 insertions(+), 43 deletions(-) diff --git a/server/vbv_lernwelt/dashboard/graphql/queries.py b/server/vbv_lernwelt/dashboard/graphql/queries.py index 797c0b4c..6e220f98 100644 --- a/server/vbv_lernwelt/dashboard/graphql/queries.py +++ b/server/vbv_lernwelt/dashboard/graphql/queries.py @@ -1,5 +1,9 @@ +from typing import Set, Dict, List, Tuple + import graphene +from vbv_lernwelt.core.admin import User +from vbv_lernwelt.course.models import CourseSessionUser from vbv_lernwelt.course.models import Course, CourseSession from vbv_lernwelt.course_session_group.models import CourseSessionGroup from vbv_lernwelt.dashboard.graphql.types.dashboard import ( @@ -10,7 +14,6 @@ from vbv_lernwelt.dashboard.graphql.types.dashboard import ( from vbv_lernwelt.iam.permissions import ( can_view_course_session, can_view_course_session_group_statistics, - can_view_course_session_progress, ) @@ -38,48 +41,86 @@ class DashboardQuery(graphene.ObjectType): return None return CourseStatisticsType( - id=course.id, + id=course.id, # noqa course_title=course.title, # noqa - course_session_selection_ids=list(course_session_ids), - ) # noqa + course_session_selection_ids=list(course_session_ids), # noqa + ) def resolve_dashboard_config(root, info): # noqa user = info.context.user - course_index = set() - dashboards = [] + ( + statistic_dashboards, + statistics_dashboard_course_ids, + ) = get_user_statistics_dashboards(user=user) - for group in CourseSessionGroup.objects.all(): - if can_view_course_session_group_statistics(user=user, group=group): - course = group.course - course_index.add(course) - dashboards.append( - { - "id": str(course.id), - "name": course.title, - "dashboard_type": DashboardType.STATISTICS_DASHBOARD, - } - ) + course_session_dashboards = get_user_course_session_dashboards( + user=user, exclude_course_ids=statistics_dashboard_course_ids + ) - for course_session in CourseSession.objects.exclude(course__in=course_index): - course = course_session.course - if can_view_course_session(user=user, course_session=course_session): - dashboards.append( - { - "id": str(course.id), - "name": course.title, - "dashboard_type": DashboardType.SIMPLE_LIST_DASHBOARD, - } - ) - if can_view_course_session_progress( - user=user, course_session=course_session - ): - dashboards.append( - { - "id": str(course.id), - "name": course.title, - "dashboard_type": DashboardType.PROGRESS_DASHBOARD, - } - ) + return statistic_dashboards + course_session_dashboards - return dashboards + +def get_user_statistics_dashboards(user: User) -> Tuple[List[Dict[str, str]], Set[int]]: + course_index = set() + dashboards = [] + + for group in CourseSessionGroup.objects.all(): + if can_view_course_session_group_statistics(user=user, group=group): + course = group.course + course_index.add(course) + dashboards.append( + { + "id": str(course.id), + "name": course.title, + "dashboard_type": DashboardType.STATISTICS_DASHBOARD, + } + ) + + return dashboards, course_index + + +def get_user_course_session_dashboards( + user: User, exclude_course_ids: Set[int] +) -> List[Dict[str, str]]: + """ + Edge case: what do we show to users with access to multiple + sessions of a course, but with varying permissions? + -> We just show the simple list dashboard for now. + """ + + dashboards = [] + + course_sessions = CourseSession.objects.exclude(course__in=exclude_course_ids) + roles_by_course: Dict[Course, Set[DashboardType]] = {} + + for course_session in course_sessions: + if can_view_course_session(user=user, course_session=course_session): + role = CourseSessionUser.objects.get( + course_session=course_session, user=user + ).role + roles_by_course.setdefault(course_session.course, set()) + roles_by_course[course_session.course].add(role) + + for course, roles in roles_by_course.items(): + resolved_dashboard_type = None + + if len(roles) == 1: + course_role = roles.pop() + if course_role == CourseSessionUser.Role.EXPERT: + resolved_dashboard_type = DashboardType.SIMPLE_LIST_DASHBOARD + elif course_role == CourseSessionUser.Role.MEMBER: + resolved_dashboard_type = DashboardType.PROGRESS_DASHBOARD + else: + # fallback: just go with simple list dashboard + resolved_dashboard_type = DashboardType.SIMPLE_LIST_DASHBOARD + + dashboards.append( + { + "id": str(course.id), + "name": course.title, + "dashboard_type": resolved_dashboard_type, + } + ) + + return dashboards diff --git a/server/vbv_lernwelt/dashboard/tests/graphql/test_dashboard.py b/server/vbv_lernwelt/dashboard/tests/graphql/test_dashboard.py index e7b4de92..d6085835 100644 --- a/server/vbv_lernwelt/dashboard/tests/graphql/test_dashboard.py +++ b/server/vbv_lernwelt/dashboard/tests/graphql/test_dashboard.py @@ -18,20 +18,35 @@ class DashboardTestCase(GraphQLTestCase): # GIVEN course_1, _ = create_course("Test Course 1") course_2, _ = create_course("Test Course 2") + course_3, _ = create_course("Test Course 3") cs_1 = create_course_session(course=course_1, title="Test Course 1 Session") cs_2 = create_course_session(course=course_2, title="Test Course 2 Session") - # Member of course 1 (via cs_1) - # Supervisor of course 2 (via cs_2) + cs_3_a = create_course_session(course=course_3, title="CS 3 A (as member)") + cs_3_b = create_course_session(course=course_3, title="CS 3 B (as expert)") + supervisor = create_user("supervisor") + # CS 1 add_course_session_user( course_session=cs_1, user=supervisor, role=CourseSessionUser.Role.MEMBER ) - group = create_course_session_group(course_session=cs_2) - add_course_session_group_supervisor(group=group, user=supervisor) + # CS 2 + add_course_session_group_supervisor( + group=create_course_session_group(course_session=cs_2), user=supervisor + ) + + # CS 3 A + add_course_session_user( + course_session=cs_3_a, user=supervisor, role=CourseSessionUser.Role.MEMBER + ) + + # CS 3 B + add_course_session_user( + course_session=cs_3_b, user=supervisor, role=CourseSessionUser.Role.EXPERT + ) self.client.force_login(supervisor) @@ -58,7 +73,7 @@ class DashboardTestCase(GraphQLTestCase): ) self.assertIsNotNone(course_1_config) self.assertEqual(course_1_config["name"], course_1.title) - self.assertEqual(course_1_config["dashboard_type"], "SIMPLE_LIST_DASHBOARD") + self.assertEqual(course_1_config["dashboard_type"], "PROGRESS_DASHBOARD") course_2_config = find_dashboard_config_by_course_id( dashboard_config, course_2.id @@ -67,6 +82,13 @@ class DashboardTestCase(GraphQLTestCase): self.assertEqual(course_2_config["name"], course_2.title) self.assertEqual(course_2_config["dashboard_type"], "STATISTICS_DASHBOARD") + course_3_config = find_dashboard_config_by_course_id( + dashboard_config, course_3.id + ) + self.assertIsNotNone(course_3_config) + self.assertEqual(course_3_config["name"], course_3.title) + self.assertEqual(course_3_config["dashboard_type"], "SIMPLE_LIST_DASHBOARD") + def test_course_statistics_deny_not_allowed_user(self): # GIVEN disallowed_user = create_user("1337_hacker_schorsch") From 228e0c8f31cb7826ca2460925ed30710c3a1ec04 Mon Sep 17 00:00:00 2001 From: Livio Bieri Date: Wed, 25 Oct 2023 12:49:34 +0200 Subject: [PATCH 25/85] chore: also expose course slug --- server/vbv_lernwelt/dashboard/graphql/queries.py | 1 + server/vbv_lernwelt/dashboard/graphql/types/dashboard.py | 1 + server/vbv_lernwelt/dashboard/tests/graphql/test_dashboard.py | 2 ++ 3 files changed, 4 insertions(+) diff --git a/server/vbv_lernwelt/dashboard/graphql/queries.py b/server/vbv_lernwelt/dashboard/graphql/queries.py index 6e220f98..eb8916b1 100644 --- a/server/vbv_lernwelt/dashboard/graphql/queries.py +++ b/server/vbv_lernwelt/dashboard/graphql/queries.py @@ -43,6 +43,7 @@ class DashboardQuery(graphene.ObjectType): return CourseStatisticsType( id=course.id, # noqa course_title=course.title, # noqa + course_slug=course.slug, # noqa course_session_selection_ids=list(course_session_ids), # noqa ) diff --git a/server/vbv_lernwelt/dashboard/graphql/types/dashboard.py b/server/vbv_lernwelt/dashboard/graphql/types/dashboard.py index eb2a8ee9..12dcdb1e 100644 --- a/server/vbv_lernwelt/dashboard/graphql/types/dashboard.py +++ b/server/vbv_lernwelt/dashboard/graphql/types/dashboard.py @@ -53,6 +53,7 @@ class DashboardConfigType(graphene.ObjectType): class CourseStatisticsType(graphene.ObjectType): id = graphene.ID(required=True) course_title = graphene.String(required=True) + course_slug = graphene.String(required=True) course_session_properties = graphene.Field(CourseSessionProperties, required=True) course_session_selection_ids = graphene.List(graphene.ID, required=True) course_session_selection_metrics = graphene.Field( diff --git a/server/vbv_lernwelt/dashboard/tests/graphql/test_dashboard.py b/server/vbv_lernwelt/dashboard/tests/graphql/test_dashboard.py index d6085835..62158b4c 100644 --- a/server/vbv_lernwelt/dashboard/tests/graphql/test_dashboard.py +++ b/server/vbv_lernwelt/dashboard/tests/graphql/test_dashboard.py @@ -135,6 +135,7 @@ class DashboardTestCase(GraphQLTestCase): course_statistics(course_id: $course_id) {{ id course_title + course_slug }} }} """ @@ -150,6 +151,7 @@ class DashboardTestCase(GraphQLTestCase): self.assertEqual(course_statistics["id"], str(course_2.id)) self.assertEqual(course_statistics["course_title"], course_2.title) + self.assertEqual(course_statistics["course_slug"], course_2.slug) def find_dashboard_config_by_course_id(dashboard_configs, course_id): From a97628c28cdf664471cfdc5cbc04f352b505af86 Mon Sep 17 00:00:00 2001 From: Livio Bieri Date: Wed, 25 Oct 2023 12:58:56 +0200 Subject: [PATCH 26/85] chore: reverts slug at wrong place, adds slug to config --- server/vbv_lernwelt/dashboard/graphql/queries.py | 5 +++-- server/vbv_lernwelt/dashboard/graphql/types/dashboard.py | 2 +- .../vbv_lernwelt/dashboard/tests/graphql/test_dashboard.py | 6 ++++-- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/server/vbv_lernwelt/dashboard/graphql/queries.py b/server/vbv_lernwelt/dashboard/graphql/queries.py index eb8916b1..bc32720f 100644 --- a/server/vbv_lernwelt/dashboard/graphql/queries.py +++ b/server/vbv_lernwelt/dashboard/graphql/queries.py @@ -3,8 +3,8 @@ from typing import Set, Dict, List, Tuple import graphene from vbv_lernwelt.core.admin import User -from vbv_lernwelt.course.models import CourseSessionUser from vbv_lernwelt.course.models import Course, CourseSession +from vbv_lernwelt.course.models import CourseSessionUser from vbv_lernwelt.course_session_group.models import CourseSessionGroup from vbv_lernwelt.dashboard.graphql.types.dashboard import ( CourseStatisticsType, @@ -43,7 +43,6 @@ class DashboardQuery(graphene.ObjectType): return CourseStatisticsType( id=course.id, # noqa course_title=course.title, # noqa - course_slug=course.slug, # noqa course_session_selection_ids=list(course_session_ids), # noqa ) @@ -74,6 +73,7 @@ def get_user_statistics_dashboards(user: User) -> Tuple[List[Dict[str, str]], Se { "id": str(course.id), "name": course.title, + "slug": course.slug, "dashboard_type": DashboardType.STATISTICS_DASHBOARD, } ) @@ -120,6 +120,7 @@ def get_user_course_session_dashboards( { "id": str(course.id), "name": course.title, + "slug": course.slug, "dashboard_type": resolved_dashboard_type, } ) diff --git a/server/vbv_lernwelt/dashboard/graphql/types/dashboard.py b/server/vbv_lernwelt/dashboard/graphql/types/dashboard.py index 12dcdb1e..73a4b1ac 100644 --- a/server/vbv_lernwelt/dashboard/graphql/types/dashboard.py +++ b/server/vbv_lernwelt/dashboard/graphql/types/dashboard.py @@ -47,13 +47,13 @@ class DashboardType(Enum): class DashboardConfigType(graphene.ObjectType): id = graphene.ID(required=True) name = graphene.String(required=True) + slug = graphene.String(required=True) dashboard_type = graphene.Field(DashboardType, required=True) class CourseStatisticsType(graphene.ObjectType): id = graphene.ID(required=True) course_title = graphene.String(required=True) - course_slug = graphene.String(required=True) course_session_properties = graphene.Field(CourseSessionProperties, required=True) course_session_selection_ids = graphene.List(graphene.ID, required=True) course_session_selection_metrics = graphene.Field( diff --git a/server/vbv_lernwelt/dashboard/tests/graphql/test_dashboard.py b/server/vbv_lernwelt/dashboard/tests/graphql/test_dashboard.py index 62158b4c..8c638880 100644 --- a/server/vbv_lernwelt/dashboard/tests/graphql/test_dashboard.py +++ b/server/vbv_lernwelt/dashboard/tests/graphql/test_dashboard.py @@ -55,6 +55,7 @@ class DashboardTestCase(GraphQLTestCase): dashboard_config { id name + slug dashboard_type } } @@ -73,6 +74,7 @@ class DashboardTestCase(GraphQLTestCase): ) self.assertIsNotNone(course_1_config) self.assertEqual(course_1_config["name"], course_1.title) + self.assertEqual(course_1_config["slug"], course_1.slug) self.assertEqual(course_1_config["dashboard_type"], "PROGRESS_DASHBOARD") course_2_config = find_dashboard_config_by_course_id( @@ -80,6 +82,7 @@ class DashboardTestCase(GraphQLTestCase): ) self.assertIsNotNone(course_2_config) self.assertEqual(course_2_config["name"], course_2.title) + self.assertEqual(course_2_config["slug"], course_2.slug) self.assertEqual(course_2_config["dashboard_type"], "STATISTICS_DASHBOARD") course_3_config = find_dashboard_config_by_course_id( @@ -87,6 +90,7 @@ class DashboardTestCase(GraphQLTestCase): ) self.assertIsNotNone(course_3_config) self.assertEqual(course_3_config["name"], course_3.title) + self.assertEqual(course_3_config["slug"], course_3.slug) self.assertEqual(course_3_config["dashboard_type"], "SIMPLE_LIST_DASHBOARD") def test_course_statistics_deny_not_allowed_user(self): @@ -135,7 +139,6 @@ class DashboardTestCase(GraphQLTestCase): course_statistics(course_id: $course_id) {{ id course_title - course_slug }} }} """ @@ -151,7 +154,6 @@ class DashboardTestCase(GraphQLTestCase): self.assertEqual(course_statistics["id"], str(course_2.id)) self.assertEqual(course_statistics["course_title"], course_2.title) - self.assertEqual(course_statistics["course_slug"], course_2.slug) def find_dashboard_config_by_course_id(dashboard_configs, course_id): From 2908e3cbf048fd88ce4830556a0cb1b8c7a5e70e Mon Sep 17 00:00:00 2001 From: Livio Bieri Date: Wed, 25 Oct 2023 13:52:06 +0200 Subject: [PATCH 27/85] chore: add course progress for progress dashboard --- .../vbv_lernwelt/dashboard/graphql/queries.py | 33 ++++++++++++- .../dashboard/graphql/types/dashboard.py | 9 +++- .../dashboard/tests/graphql/test_dashboard.py | 49 +++++++++++++++++++ 3 files changed, 87 insertions(+), 4 deletions(-) diff --git a/server/vbv_lernwelt/dashboard/graphql/queries.py b/server/vbv_lernwelt/dashboard/graphql/queries.py index bc32720f..c37c5150 100644 --- a/server/vbv_lernwelt/dashboard/graphql/queries.py +++ b/server/vbv_lernwelt/dashboard/graphql/queries.py @@ -9,11 +9,11 @@ from vbv_lernwelt.course_session_group.models import CourseSessionGroup from vbv_lernwelt.dashboard.graphql.types.dashboard import ( CourseStatisticsType, DashboardConfigType, - DashboardType, + DashboardType, CourseProgressType, ) from vbv_lernwelt.iam.permissions import ( can_view_course_session, - can_view_course_session_group_statistics, + can_view_course_session_group_statistics, can_view_course_session_progress, ) @@ -21,6 +21,11 @@ class DashboardQuery(graphene.ObjectType): course_statistics = graphene.Field( CourseStatisticsType, course_id=graphene.ID(required=True) ) + + course_progress = graphene.Field( + CourseProgressType, course_id=graphene.ID(required=True) + ) + dashboard_config = graphene.List( graphene.NonNull(DashboardConfigType), required=True ) @@ -60,6 +65,30 @@ class DashboardQuery(graphene.ObjectType): return statistic_dashboards + course_session_dashboards + def resolve_course_progress(root, info, course_id: str): # noqa + """ + Slightly fragile but could be good enough: most only have one + course session per course anyway but if there are multiple, we + just pick the newest one (by generation) as best guess. + """ + + user = info.context.user + newest: CourseSession | None = None + + for course_session in CourseSession.objects.filter(course_id=course_id): + if can_view_course_session_progress(user=user, course_session=course_session): + generation_newest = newest.generation if newest else None + if generation_newest is None or course_session.generation > generation_newest: + newest = course_session + + if not newest: + return None + + return CourseProgressType( + id=course_id, # noqa + session_to_continue_id=newest.id # noqa + ) + def get_user_statistics_dashboards(user: User) -> Tuple[List[Dict[str, str]], Set[int]]: course_index = set() diff --git a/server/vbv_lernwelt/dashboard/graphql/types/dashboard.py b/server/vbv_lernwelt/dashboard/graphql/types/dashboard.py index 73a4b1ac..45c82f24 100644 --- a/server/vbv_lernwelt/dashboard/graphql/types/dashboard.py +++ b/server/vbv_lernwelt/dashboard/graphql/types/dashboard.py @@ -45,14 +45,19 @@ class DashboardType(Enum): class DashboardConfigType(graphene.ObjectType): - id = graphene.ID(required=True) + id = graphene.ID(required=True) # course_id, named id for urql name = graphene.String(required=True) slug = graphene.String(required=True) dashboard_type = graphene.Field(DashboardType, required=True) +class CourseProgressType(graphene.ObjectType): + id = graphene.ID(required=True) # course_id, named id for urql + session_to_continue_id = graphene.ID(required=True) + + class CourseStatisticsType(graphene.ObjectType): - id = graphene.ID(required=True) + id = graphene.ID(required=True) # course_id, named id for urql course_title = graphene.String(required=True) course_session_properties = graphene.Field(CourseSessionProperties, required=True) course_session_selection_ids = graphene.List(graphene.ID, required=True) diff --git a/server/vbv_lernwelt/dashboard/tests/graphql/test_dashboard.py b/server/vbv_lernwelt/dashboard/tests/graphql/test_dashboard.py index 8c638880..ab1dc5d7 100644 --- a/server/vbv_lernwelt/dashboard/tests/graphql/test_dashboard.py +++ b/server/vbv_lernwelt/dashboard/tests/graphql/test_dashboard.py @@ -14,6 +14,55 @@ from vbv_lernwelt.dashboard.tests.graphql.utils import ( class DashboardTestCase(GraphQLTestCase): GRAPHQL_URL = "/server/graphql/" + def test_course_progress(self): + # GIVEN + course, _ = create_course("Test Course") + + cs_1 = create_course_session( + course=course, title="Test Course Session 1", generation="" + ) + cs_2 = create_course_session( + course=course, title="Test Course Session 2", generation="2020" + ) + cs_3 = create_course_session( + course=course, title="Test Course Session 3", generation="1984" + ) + + member = create_user("sepp") + + add_course_session_user( + course_session=cs_1, user=member, role=CourseSessionUser.Role.MEMBER + ) + add_course_session_user( + course_session=cs_2, user=member, role=CourseSessionUser.Role.MEMBER + ) + add_course_session_user( + course_session=cs_3, user=member, role=CourseSessionUser.Role.MEMBER + ) + + self.client.force_login(member) + + query = f"""query($course_id: ID!) {{ + course_progress(course_id: $course_id) {{ + id + session_to_continue_id + }} + }} + """ + + variables = {"course_id": str(course.id)} + + # WHEN + response = self.query(query, variables=variables) + + # THEN + self.assertResponseNoErrors(response) + + course_progress = response.json()["data"]["course_progress"] + + self.assertEqual(course_progress["id"], str(course.id)) + self.assertEqual(course_progress["session_to_continue_id"], str(cs_2.id)) + def test_dashboard_config(self): # GIVEN course_1, _ = create_course("Test Course 1") From 9d6a0a561b2655779c1e34cf7c6a7d4f1816d296 Mon Sep 17 00:00:00 2001 From: Reto Aebersold Date: Wed, 25 Oct 2023 16:40:41 +0200 Subject: [PATCH 28/85] feat: add dashboard page variants --- .../src/components/dashboard/SimpleDates.vue | 21 ++++++++ client/src/gql/gql.ts | 9 +++- client/src/gql/graphql.ts | 25 ++++++++- client/src/gql/schema.graphql | 7 +++ client/src/gql/typenames.ts | 1 + client/src/graphql/queries.ts | 10 ++++ client/src/pages/dashboard/DashboardPage.vue | 53 +++++++++++++++++++ client/src/pages/dashboard/ProgressPage.vue | 52 ++++++++++++++++++ client/src/pages/dashboard/SimpleListPage.vue | 25 +++++++++ client/src/pages/dashboard/StatisticPage.vue | 7 +++ client/src/router/index.ts | 2 +- client/src/stores/dashboard.ts | 30 +++++++++++ .../vbv_lernwelt/dashboard/graphql/queries.py | 43 ++++++++++----- 13 files changed, 267 insertions(+), 18 deletions(-) create mode 100644 client/src/components/dashboard/SimpleDates.vue create mode 100644 client/src/pages/dashboard/DashboardPage.vue create mode 100644 client/src/pages/dashboard/ProgressPage.vue create mode 100644 client/src/pages/dashboard/SimpleListPage.vue create mode 100644 client/src/pages/dashboard/StatisticPage.vue create mode 100644 client/src/stores/dashboard.ts diff --git a/client/src/components/dashboard/SimpleDates.vue b/client/src/components/dashboard/SimpleDates.vue new file mode 100644 index 00000000..093b2cda --- /dev/null +++ b/client/src/components/dashboard/SimpleDates.vue @@ -0,0 +1,21 @@ + + + + + diff --git a/client/src/gql/gql.ts b/client/src/gql/gql.ts index b6ef5e52..340ab070 100644 --- a/client/src/gql/gql.ts +++ b/client/src/gql/gql.ts @@ -21,7 +21,8 @@ const documents = { "\n query competenceCertificateQuery($courseSlug: String!, $courseSessionId: ID!) {\n competence_certificate_list(course_slug: $courseSlug) {\n ...CoursePageFields\n competence_certificates {\n ...CoursePageFields\n assignments {\n ...CoursePageFields\n assignment_type\n max_points\n completion(course_session_id: $courseSessionId) {\n id\n completion_status\n submitted_at\n evaluation_points\n evaluation_max_points\n evaluation_passed\n }\n learning_content {\n ...CoursePageFields\n circle {\n id\n title\n slug\n }\n }\n }\n }\n }\n }\n": types.CompetenceCertificateQueryDocument, "\n query courseSessionDetail($courseSessionId: ID!) {\n course_session(id: $courseSessionId) {\n id\n title\n course {\n id\n title\n slug\n }\n users {\n id\n user_id\n first_name\n last_name\n email\n avatar_url\n role\n circles {\n id\n title\n slug\n }\n }\n attendance_courses {\n id\n location\n trainer\n due_date {\n id\n start\n end\n }\n learning_content_id\n learning_content {\n id\n title\n circle {\n id\n title\n slug\n }\n }\n }\n assignments {\n id\n submission_deadline {\n id\n start\n }\n evaluation_deadline {\n id\n start\n }\n learning_content {\n id\n title\n content_assignment {\n id\n title\n assignment_type\n }\n }\n }\n edoniq_tests {\n id\n deadline {\n id\n start\n end\n }\n learning_content {\n id\n title\n content_assignment {\n id\n title\n assignment_type\n }\n }\n }\n }\n }\n": types.CourseSessionDetailDocument, "\n query courseQuery($slug: String!) {\n course(slug: $slug) {\n id\n title\n slug\n category_name\n action_competences {\n competence_id\n ...CoursePageFields\n performance_criteria {\n competence_id\n learning_unit {\n id\n slug\n evaluate_url\n }\n ...CoursePageFields\n }\n }\n learning_path {\n ...CoursePageFields\n topics {\n is_visible\n ...CoursePageFields\n circles {\n description\n goals\n ...CoursePageFields\n learning_sequences {\n icon\n ...CoursePageFields\n learning_units {\n evaluate_url\n ...CoursePageFields\n performance_criteria {\n ...CoursePageFields\n }\n learning_contents {\n can_user_self_toggle_course_completion\n content_url\n minutes\n description\n ...CoursePageFields\n ... on LearningContentAssignmentObjectType {\n assignment_type\n content_assignment {\n id\n }\n competence_certificate {\n ...CoursePageFields\n }\n }\n ... on LearningContentEdoniqTestObjectType {\n checkbox_text\n has_extended_time_test\n content_assignment {\n id\n }\n competence_certificate {\n ...CoursePageFields\n }\n }\n ... on LearningContentRichTextObjectType {\n text\n }\n }\n }\n }\n }\n }\n }\n }\n }\n": types.CourseQueryDocument, - "\n query dashboardConfig {\n dashboard_config {\n id\n name\n dashboard_type\n }\n }\n": types.DashboardConfigDocument, + "\n query dashboardConfig {\n dashboard_config {\n id\n slug\n name\n dashboard_type\n }\n }\n": types.DashboardConfigDocument, + "\n query dashboardProgress($courseId: ID!) {\n course_progress(course_id: $courseId) {\n id\n session_to_continue_id\n }\n }\n": types.DashboardProgressDocument, "\n mutation SendFeedbackMutation(\n $courseSessionId: ID!\n $learningContentId: ID!\n $data: GenericScalar!\n $submitted: Boolean\n ) {\n send_feedback(\n course_session_id: $courseSessionId\n learning_content_page_id: $learningContentId\n data: $data\n submitted: $submitted\n ) {\n feedback_response {\n id\n data\n submitted\n }\n errors {\n field\n messages\n }\n }\n }\n": types.SendFeedbackMutationDocument, }; @@ -74,7 +75,11 @@ export function graphql(source: "\n query courseQuery($slug: String!) {\n co /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ -export function graphql(source: "\n query dashboardConfig {\n dashboard_config {\n id\n name\n dashboard_type\n }\n }\n"): (typeof documents)["\n query dashboardConfig {\n dashboard_config {\n id\n name\n dashboard_type\n }\n }\n"]; +export function graphql(source: "\n query dashboardConfig {\n dashboard_config {\n id\n slug\n name\n dashboard_type\n }\n }\n"): (typeof documents)["\n query dashboardConfig {\n dashboard_config {\n id\n slug\n name\n dashboard_type\n }\n }\n"]; +/** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function graphql(source: "\n query dashboardProgress($courseId: ID!) {\n course_progress(course_id: $courseId) {\n id\n session_to_continue_id\n }\n }\n"): (typeof documents)["\n query dashboardProgress($courseId: ID!) {\n course_progress(course_id: $courseId) {\n id\n session_to_continue_id\n }\n }\n"]; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ diff --git a/client/src/gql/graphql.ts b/client/src/gql/graphql.ts index af755314..9f36ebb1 100644 --- a/client/src/gql/graphql.ts +++ b/client/src/gql/graphql.ts @@ -341,6 +341,12 @@ export type CoursePageInterface = { translation_key: Scalars['String']['output']; }; +export type CourseProgressType = { + __typename?: 'CourseProgressType'; + id: Scalars['ID']['output']; + session_to_continue_id: Scalars['ID']['output']; +}; + export type CourseSessionAssignmentObjectType = { __typename?: 'CourseSessionAssignmentObjectType'; course_session_id: Scalars['ID']['output']; @@ -444,6 +450,7 @@ export type DashboardConfigType = { dashboard_type: DashboardType; id: Scalars['ID']['output']; name: Scalars['String']['output']; + slug: Scalars['String']['output']; }; export type DashboardType = @@ -824,6 +831,7 @@ export type Query = { competence_certificate?: Maybe; competence_certificate_list?: Maybe; course?: Maybe; + course_progress?: Maybe; course_session?: Maybe; course_session_attendance_course?: Maybe; course_statistics?: Maybe; @@ -876,6 +884,11 @@ export type QueryCourseArgs = { }; +export type QueryCourseProgressArgs = { + course_id: Scalars['ID']['input']; +}; + + export type QueryCourseSessionArgs = { id?: InputMaybe; }; @@ -1140,7 +1153,14 @@ export type CourseQueryQuery = { __typename?: 'Query', course?: { __typename?: ' export type DashboardConfigQueryVariables = Exact<{ [key: string]: never; }>; -export type DashboardConfigQuery = { __typename?: 'Query', dashboard_config: Array<{ __typename?: 'DashboardConfigType', id: string, name: string, dashboard_type: DashboardType }> }; +export type DashboardConfigQuery = { __typename?: 'Query', dashboard_config: Array<{ __typename?: 'DashboardConfigType', id: string, slug: string, name: string, dashboard_type: DashboardType }> }; + +export type DashboardProgressQueryVariables = Exact<{ + courseId: Scalars['ID']['input']; +}>; + + +export type DashboardProgressQuery = { __typename?: 'Query', course_progress?: { __typename?: 'CourseProgressType', id: string, session_to_continue_id: string } | null }; export type SendFeedbackMutationMutationVariables = Exact<{ courseSessionId: Scalars['ID']['input']; @@ -1160,5 +1180,6 @@ export const AssignmentCompletionQueryDocument = {"kind":"Document","definitions export const CompetenceCertificateQueryDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"competenceCertificateQuery"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"courseSlug"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"courseSessionId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"competence_certificate_list"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"course_slug"},"value":{"kind":"Variable","name":{"kind":"Name","value":"courseSlug"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"CoursePageFields"}},{"kind":"Field","name":{"kind":"Name","value":"competence_certificates"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"CoursePageFields"}},{"kind":"Field","name":{"kind":"Name","value":"assignments"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"CoursePageFields"}},{"kind":"Field","name":{"kind":"Name","value":"assignment_type"}},{"kind":"Field","name":{"kind":"Name","value":"max_points"}},{"kind":"Field","name":{"kind":"Name","value":"completion"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"course_session_id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"courseSessionId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"completion_status"}},{"kind":"Field","name":{"kind":"Name","value":"submitted_at"}},{"kind":"Field","name":{"kind":"Name","value":"evaluation_points"}},{"kind":"Field","name":{"kind":"Name","value":"evaluation_max_points"}},{"kind":"Field","name":{"kind":"Name","value":"evaluation_passed"}}]}},{"kind":"Field","name":{"kind":"Name","value":"learning_content"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"CoursePageFields"}},{"kind":"Field","name":{"kind":"Name","value":"circle"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}}]}}]}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"CoursePageFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"CoursePageInterface"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"content_type"}},{"kind":"Field","name":{"kind":"Name","value":"frontend_url"}}]}}]} as unknown as DocumentNode; export const CourseSessionDetailDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"courseSessionDetail"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"courseSessionId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"course_session"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"courseSessionId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"course"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}}]}},{"kind":"Field","name":{"kind":"Name","value":"users"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"user_id"}},{"kind":"Field","name":{"kind":"Name","value":"first_name"}},{"kind":"Field","name":{"kind":"Name","value":"last_name"}},{"kind":"Field","name":{"kind":"Name","value":"email"}},{"kind":"Field","name":{"kind":"Name","value":"avatar_url"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"circles"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"attendance_courses"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"location"}},{"kind":"Field","name":{"kind":"Name","value":"trainer"}},{"kind":"Field","name":{"kind":"Name","value":"due_date"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"start"}},{"kind":"Field","name":{"kind":"Name","value":"end"}}]}},{"kind":"Field","name":{"kind":"Name","value":"learning_content_id"}},{"kind":"Field","name":{"kind":"Name","value":"learning_content"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"circle"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"assignments"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"submission_deadline"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"start"}}]}},{"kind":"Field","name":{"kind":"Name","value":"evaluation_deadline"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"start"}}]}},{"kind":"Field","name":{"kind":"Name","value":"learning_content"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"content_assignment"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"assignment_type"}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"edoniq_tests"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"deadline"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"start"}},{"kind":"Field","name":{"kind":"Name","value":"end"}}]}},{"kind":"Field","name":{"kind":"Name","value":"learning_content"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"content_assignment"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"assignment_type"}}]}}]}}]}}]}}]}}]} as unknown as DocumentNode; export const CourseQueryDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"courseQuery"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"slug"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"course"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"slug"},"value":{"kind":"Variable","name":{"kind":"Name","value":"slug"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"category_name"}},{"kind":"Field","name":{"kind":"Name","value":"action_competences"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"competence_id"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"CoursePageFields"}},{"kind":"Field","name":{"kind":"Name","value":"performance_criteria"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"competence_id"}},{"kind":"Field","name":{"kind":"Name","value":"learning_unit"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"evaluate_url"}}]}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"CoursePageFields"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"learning_path"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"CoursePageFields"}},{"kind":"Field","name":{"kind":"Name","value":"topics"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"is_visible"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"CoursePageFields"}},{"kind":"Field","name":{"kind":"Name","value":"circles"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"goals"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"CoursePageFields"}},{"kind":"Field","name":{"kind":"Name","value":"learning_sequences"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"icon"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"CoursePageFields"}},{"kind":"Field","name":{"kind":"Name","value":"learning_units"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"evaluate_url"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"CoursePageFields"}},{"kind":"Field","name":{"kind":"Name","value":"performance_criteria"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"CoursePageFields"}}]}},{"kind":"Field","name":{"kind":"Name","value":"learning_contents"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"can_user_self_toggle_course_completion"}},{"kind":"Field","name":{"kind":"Name","value":"content_url"}},{"kind":"Field","name":{"kind":"Name","value":"minutes"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"CoursePageFields"}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"LearningContentAssignmentObjectType"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"assignment_type"}},{"kind":"Field","name":{"kind":"Name","value":"content_assignment"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"Field","name":{"kind":"Name","value":"competence_certificate"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"CoursePageFields"}}]}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"LearningContentEdoniqTestObjectType"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"checkbox_text"}},{"kind":"Field","name":{"kind":"Name","value":"has_extended_time_test"}},{"kind":"Field","name":{"kind":"Name","value":"content_assignment"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"Field","name":{"kind":"Name","value":"competence_certificate"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"CoursePageFields"}}]}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"LearningContentRichTextObjectType"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"text"}}]}}]}}]}}]}}]}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"CoursePageFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"CoursePageInterface"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"content_type"}},{"kind":"Field","name":{"kind":"Name","value":"frontend_url"}}]}}]} as unknown as DocumentNode; -export const DashboardConfigDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"dashboardConfig"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"dashboard_config"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"dashboard_type"}}]}}]}}]} as unknown as DocumentNode; +export const DashboardConfigDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"dashboardConfig"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"dashboard_config"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"dashboard_type"}}]}}]}}]} as unknown as DocumentNode; +export const DashboardProgressDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"dashboardProgress"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"courseId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"course_progress"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"course_id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"courseId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"session_to_continue_id"}}]}}]}}]} as unknown as DocumentNode; export const SendFeedbackMutationDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"SendFeedbackMutation"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"courseSessionId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"learningContentId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"data"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"GenericScalar"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"submitted"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Boolean"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"send_feedback"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"course_session_id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"courseSessionId"}}},{"kind":"Argument","name":{"kind":"Name","value":"learning_content_page_id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"learningContentId"}}},{"kind":"Argument","name":{"kind":"Name","value":"data"},"value":{"kind":"Variable","name":{"kind":"Name","value":"data"}}},{"kind":"Argument","name":{"kind":"Name","value":"submitted"},"value":{"kind":"Variable","name":{"kind":"Name","value":"submitted"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"feedback_response"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"data"}},{"kind":"Field","name":{"kind":"Name","value":"submitted"}}]}},{"kind":"Field","name":{"kind":"Name","value":"errors"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"field"}},{"kind":"Field","name":{"kind":"Name","value":"messages"}}]}}]}}]}}]} as unknown as DocumentNode; \ No newline at end of file diff --git a/client/src/gql/schema.graphql b/client/src/gql/schema.graphql index e50c8055..430372e3 100644 --- a/client/src/gql/schema.graphql +++ b/client/src/gql/schema.graphql @@ -1,5 +1,6 @@ type Query { course_statistics(course_id: ID!): CourseStatisticsType + course_progress(course_id: ID!): CourseProgressType dashboard_config: [DashboardConfigType!]! learning_path(id: ID, slug: String, course_id: ID, course_slug: String): LearningPathObjectType course_session_attendance_course(id: ID!, assignment_user_id: ID): CourseSessionAttendanceCourseObjectType @@ -150,9 +151,15 @@ type CompletionSummary { fail_total: Int! } +type CourseProgressType { + id: ID! + session_to_continue_id: ID! +} + type DashboardConfigType { id: ID! name: String! + slug: String! dashboard_type: DashboardType! } diff --git a/client/src/gql/typenames.ts b/client/src/gql/typenames.ts index c870470a..4231060b 100644 --- a/client/src/gql/typenames.ts +++ b/client/src/gql/typenames.ts @@ -28,6 +28,7 @@ export const CoreUserLanguageChoices = "CoreUserLanguageChoices"; export const CourseCourseSessionUserRoleChoices = "CourseCourseSessionUserRoleChoices"; export const CourseObjectType = "CourseObjectType"; export const CoursePageInterface = "CoursePageInterface"; +export const CourseProgressType = "CourseProgressType"; export const CourseSessionAssignmentObjectType = "CourseSessionAssignmentObjectType"; export const CourseSessionAttendanceCourseObjectType = "CourseSessionAttendanceCourseObjectType"; export const CourseSessionData = "CourseSessionData"; diff --git a/client/src/graphql/queries.ts b/client/src/graphql/queries.ts index 7751e093..fb7bcf3e 100644 --- a/client/src/graphql/queries.ts +++ b/client/src/graphql/queries.ts @@ -273,8 +273,18 @@ export const DASHBOARD_CONFIG = graphql(` query dashboardConfig { dashboard_config { id + slug name dashboard_type } } `); + +export const DASHBOARD_COURSE_SESSION_PROGRESS = graphql(` + query dashboardProgress($courseId: ID!) { + course_progress(course_id: $courseId) { + id + session_to_continue_id + } + } +`); diff --git a/client/src/pages/dashboard/DashboardPage.vue b/client/src/pages/dashboard/DashboardPage.vue new file mode 100644 index 00000000..5e7eaf49 --- /dev/null +++ b/client/src/pages/dashboard/DashboardPage.vue @@ -0,0 +1,53 @@ + + + diff --git a/client/src/pages/dashboard/ProgressPage.vue b/client/src/pages/dashboard/ProgressPage.vue new file mode 100644 index 00000000..5e1deab5 --- /dev/null +++ b/client/src/pages/dashboard/ProgressPage.vue @@ -0,0 +1,52 @@ + + + diff --git a/client/src/pages/dashboard/SimpleListPage.vue b/client/src/pages/dashboard/SimpleListPage.vue new file mode 100644 index 00000000..e8aedb50 --- /dev/null +++ b/client/src/pages/dashboard/SimpleListPage.vue @@ -0,0 +1,25 @@ + + + diff --git a/client/src/pages/dashboard/StatisticPage.vue b/client/src/pages/dashboard/StatisticPage.vue new file mode 100644 index 00000000..50f9a1cc --- /dev/null +++ b/client/src/pages/dashboard/StatisticPage.vue @@ -0,0 +1,7 @@ + + + + + diff --git a/client/src/router/index.ts b/client/src/router/index.ts index 805a1e9c..b0860817 100644 --- a/client/src/router/index.ts +++ b/client/src/router/index.ts @@ -1,4 +1,4 @@ -import DashboardPage from "@/pages/DashboardPage.vue"; +import DashboardPage from "@/pages/dashboard/DashboardPage.vue"; import LoginPage from "@/pages/LoginPage.vue"; import { handleCourseSessionAsQueryParam, diff --git a/client/src/stores/dashboard.ts b/client/src/stores/dashboard.ts new file mode 100644 index 00000000..388876c9 --- /dev/null +++ b/client/src/stores/dashboard.ts @@ -0,0 +1,30 @@ +import type { DashboardConfigType } from "@/gql/graphql"; +import { graphqlClient } from "@/graphql/client"; +import { DASHBOARD_CONFIG } from "@/graphql/queries"; +import { defineStore } from "pinia"; +import type { Ref } from "vue"; +import { ref } from "vue"; + +export const useDashboardStore = defineStore("dashboard", () => { + const dashboardConfigs: Ref = ref([]); + const currentDashboardConfig: Ref = ref(); + + const setCurrentDashboardConfig = (config: DashboardConfigType) => { + currentDashboardConfig.value = config; + }; + + const loadDashboardConfig = async () => { + const res = await graphqlClient.query(DASHBOARD_CONFIG, {}); + if (res.data) { + dashboardConfigs.value = res.data.dashboard_config; + currentDashboardConfig.value = res.data.dashboard_config[0]; + } + }; + + return { + dashboardConfigs, + currentDashboardConfig, + setCurrentDashboardConfig, + loadDashboardConfig, + }; +}); diff --git a/server/vbv_lernwelt/dashboard/graphql/queries.py b/server/vbv_lernwelt/dashboard/graphql/queries.py index c37c5150..4c24dc7b 100644 --- a/server/vbv_lernwelt/dashboard/graphql/queries.py +++ b/server/vbv_lernwelt/dashboard/graphql/queries.py @@ -1,19 +1,20 @@ -from typing import Set, Dict, List, Tuple +from typing import Dict, List, Set, Tuple import graphene from vbv_lernwelt.core.admin import User -from vbv_lernwelt.course.models import Course, CourseSession -from vbv_lernwelt.course.models import CourseSessionUser +from vbv_lernwelt.course.models import Course, CourseSession, CourseSessionUser from vbv_lernwelt.course_session_group.models import CourseSessionGroup from vbv_lernwelt.dashboard.graphql.types.dashboard import ( + CourseProgressType, CourseStatisticsType, DashboardConfigType, - DashboardType, CourseProgressType, + DashboardType, ) from vbv_lernwelt.iam.permissions import ( can_view_course_session, - can_view_course_session_group_statistics, can_view_course_session_progress, + can_view_course_session_group_statistics, + can_view_course_session_progress, ) @@ -46,14 +47,26 @@ class DashboardQuery(graphene.ObjectType): return None return CourseStatisticsType( - id=course.id, # noqa + id=course.id, # noqa course_title=course.title, # noqa - course_session_selection_ids=list(course_session_ids), # noqa + course_session_selection_ids=list(course_session_ids), # noqa ) def resolve_dashboard_config(root, info): # noqa user = info.context.user + if user.is_superuser: + courses = Course.objects.all().values("id", "title", "slug") + return [ + { + "id": c["id"], + "name": c["title"], + "slug": c["slug"], + "dashboard_type": DashboardType.SIMPLE_LIST_DASHBOARD, + } + for c in courses + ] + ( statistic_dashboards, statistics_dashboard_course_ids, @@ -65,7 +78,7 @@ class DashboardQuery(graphene.ObjectType): return statistic_dashboards + course_session_dashboards - def resolve_course_progress(root, info, course_id: str): # noqa + def resolve_course_progress(root, info, course_id: str): # noqa """ Slightly fragile but could be good enough: most only have one course session per course anyway but if there are multiple, we @@ -76,17 +89,21 @@ class DashboardQuery(graphene.ObjectType): newest: CourseSession | None = None for course_session in CourseSession.objects.filter(course_id=course_id): - if can_view_course_session_progress(user=user, course_session=course_session): + if can_view_course_session_progress( + user=user, course_session=course_session + ): generation_newest = newest.generation if newest else None - if generation_newest is None or course_session.generation > generation_newest: + if ( + generation_newest is None + or course_session.generation > generation_newest + ): newest = course_session if not newest: return None return CourseProgressType( - id=course_id, # noqa - session_to_continue_id=newest.id # noqa + id=course_id, session_to_continue_id=newest.id # noqa # noqa ) @@ -111,7 +128,7 @@ def get_user_statistics_dashboards(user: User) -> Tuple[List[Dict[str, str]], Se def get_user_course_session_dashboards( - user: User, exclude_course_ids: Set[int] + user: User, exclude_course_ids: Set[int] ) -> List[Dict[str, str]]: """ Edge case: what do we show to users with access to multiple From b9c622b20d5deb79e286cd3004e1f3bdd2914829 Mon Sep 17 00:00:00 2001 From: Reto Aebersold Date: Wed, 25 Oct 2023 16:42:45 +0200 Subject: [PATCH 29/85] chore: title --- client/src/pages/dashboard/ProgressPage.vue | 2 -- server/vbv_lernwelt/dashboard/graphql/queries.py | 8 ++++---- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/client/src/pages/dashboard/ProgressPage.vue b/client/src/pages/dashboard/ProgressPage.vue index 5e1deab5..cc6fa94a 100644 --- a/client/src/pages/dashboard/ProgressPage.vue +++ b/client/src/pages/dashboard/ProgressPage.vue @@ -24,8 +24,6 @@ const progress = progressQuery.data;