wip: session group statistics w/ permission

This commit is contained in:
Livio Bieri 2023-10-24 12:05:55 +02:00
parent c7920430ca
commit ca44a913c9
23 changed files with 208 additions and 63 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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)
course_session_ids = set()
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",
}
)
courses = query.distinct()
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 [
CourseStatisticsType(course_id=course.id, course_title=course.title) # noqa
for course in courses
]
return dashboards

View File

@ -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] = []

View File

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

View File

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

View File

@ -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 = []

View File

@ -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 = []

View File

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

View File

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

View File

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

View File

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

View File

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

View File

View File

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