diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/Coursebook.vue b/aleksis/apps/alsijil/frontend/components/coursebook/Coursebook.vue index 8a82714c7d8de7117005b74da5b744faf0557076..5c4537f502af6415c76cf78f43bc19a73c149d2f 100644 --- a/aleksis/apps/alsijil/frontend/components/coursebook/Coursebook.vue +++ b/aleksis/apps/alsijil/frontend/components/coursebook/Coursebook.vue @@ -65,7 +65,9 @@ <template #no-results> <CoursebookEmptyMessage icon="mdi-book-alert-outline"> - {{ $t("alsijil.coursebook.no_results", { search: $refs.iterator.search }) }} + {{ + $t("alsijil.coursebook.no_results", { search: $refs.iterator.search }) + }} </CoursebookEmptyMessage> </template> </c-r-u-d-iterator> @@ -74,8 +76,12 @@ <script> import CRUDIterator from "aleksis.core/components/generic/CRUDIterator.vue"; import DocumentationModal from "./documentation/DocumentationModal.vue"; -import {DateTime} from "luxon"; -import {coursesOfTeacher, documentationsForCoursebook, groupsByOwner,} from "./coursebook.graphql"; +import { DateTime } from "luxon"; +import { + coursesOfPerson, + documentationsForCoursebook, + groupsByPerson, +} from "./coursebook.graphql"; import CoursebookLoader from "./CoursebookLoader.vue"; import CoursebookEmptyMessage from "./CoursebookEmptyMessage.vue"; import CoursebookDateSelect from "./CoursebookDateSelect.vue"; @@ -126,10 +132,10 @@ export default { }, apollo: { groups: { - query: groupsByOwner, + query: groupsByPerson, }, courses: { - query: coursesOfTeacher, + query: coursesOfPerson, }, }, computed: { diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/CoursebookDateSelect.vue b/aleksis/apps/alsijil/frontend/components/coursebook/CoursebookDateSelect.vue index b3682de4216f7694c3b6b1f9bb1a2d23ea9a855e..ac54bff3b33861bab60ebad1928291bff66b8ddd 100644 --- a/aleksis/apps/alsijil/frontend/components/coursebook/CoursebookDateSelect.vue +++ b/aleksis/apps/alsijil/frontend/components/coursebook/CoursebookDateSelect.vue @@ -44,14 +44,14 @@ export default { } else { this.$emit("input", DateTime.fromISO(this.value).plus({ days: 1 }).toISODate()); } - } + }, }, emits: ["input", "click", "prev", "next"], -} +}; </script> <template> -<v-bottom-sheet + <v-bottom-sheet :value="true" persistent hide-overlay @@ -88,4 +88,4 @@ export default { margin: auto; max-width: 500px; } -</style> \ No newline at end of file +</style> diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/CoursebookEmptyMessage.vue b/aleksis/apps/alsijil/frontend/components/coursebook/CoursebookEmptyMessage.vue index 059d613c3b4fb0a1d6b8234ae0c74c2d7a673cec..346ecc63c272ab5c57a1da9f5e5f78b825739a79 100644 --- a/aleksis/apps/alsijil/frontend/components/coursebook/CoursebookEmptyMessage.vue +++ b/aleksis/apps/alsijil/frontend/components/coursebook/CoursebookEmptyMessage.vue @@ -14,12 +14,12 @@ </template> <script> export default { - name: 'CoursebookEmptyMessage', + name: "CoursebookEmptyMessage", props: { icon: { type: String, - default: 'mdi-book-alert-outline', + default: "mdi-book-alert-outline", }, }, -} -</script> \ No newline at end of file +}; +</script> diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/coursebook.graphql b/aleksis/apps/alsijil/frontend/components/coursebook/coursebook.graphql index 5d7adabf478caa77a3cc2f1bbf1bd5ab16fe75b0..08064df0901ce59c0de868b522c3fa141aa9ed2a 100644 --- a/aleksis/apps/alsijil/frontend/components/coursebook/coursebook.graphql +++ b/aleksis/apps/alsijil/frontend/components/coursebook/coursebook.graphql @@ -1,12 +1,12 @@ -query groupsByOwner { - groups: groupsByOwner { +query groupsByPerson { + groups: groupsByPerson { id name } } -query coursesOfTeacher { - courses: coursesOfTeacher { +query coursesOfPerson { + courses: coursesOfPerson { id name groups { diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/documentation/Documentation.vue b/aleksis/apps/alsijil/frontend/components/coursebook/documentation/Documentation.vue index 0891fab875b4e1ea2117cd1b246e4aad7565824e..07918bc166c5b0717b30f11a8d4cf8e252ebadcb 100644 --- a/aleksis/apps/alsijil/frontend/components/coursebook/documentation/Documentation.vue +++ b/aleksis/apps/alsijil/frontend/components/coursebook/documentation/Documentation.vue @@ -6,10 +6,7 @@ class="full-width d-flex flex-column align-stretch" :class="{ 'flex-md-row': 'compact' in $attrs }" > - <lesson-information - class="flex-grow-1" - :documentation="documentation" - /> + <lesson-information class="flex-grow-1" :documentation="documentation" /> <lesson-summary class="flex-grow-1" ref="summary" diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/documentation/LessonNotes.vue b/aleksis/apps/alsijil/frontend/components/coursebook/documentation/LessonNotes.vue index e67555bb1463f5fc375533801d5a3004d05df803..f5cb72e1b7195f4e14943599e76df5ab0365f332 100644 --- a/aleksis/apps/alsijil/frontend/components/coursebook/documentation/LessonNotes.vue +++ b/aleksis/apps/alsijil/frontend/components/coursebook/documentation/LessonNotes.vue @@ -1,19 +1,29 @@ <template> - <v-card-text class="d-flex align-center justify-space-between justify-md-end flex-wrap gap"> + <v-card-text + class="d-flex align-center justify-space-between justify-md-end flex-wrap gap" + > <v-chip dense color="success"> - <v-chip small dense class="mr-2" color="green darken-3 white--text">26</v-chip> + <v-chip small dense class="mr-2" color="green darken-3 white--text" + >26</v-chip + > von 30 anwesend </v-chip> <v-chip dense color="warning"> - <v-chip small dense class="mr-2" color="orange darken-3 white--text">3</v-chip> + <v-chip small dense class="mr-2" color="orange darken-3 white--text" + >3</v-chip + > entschuldigt </v-chip> <v-chip dense color="error"> - <v-chip small dense class="mr-2" color="red darken-3 white--text">1</v-chip> + <v-chip small dense class="mr-2" color="red darken-3 white--text" + >1</v-chip + > unentschuldigt </v-chip> <v-chip dense color="grey lighten-1"> - <v-chip small dense class="mr-2" color="grey darken-1 white--text">4</v-chip> + <v-chip small dense class="mr-2" color="grey darken-1 white--text" + >4</v-chip + > Hausaufgaben vergessen </v-chip> </v-card-text> diff --git a/aleksis/apps/alsijil/models.py b/aleksis/apps/alsijil/models.py index 803efbfb8227c8f66597049f099c82b52411cb18..7356d23dad2949092bb31ab292c081c06fdb1e2b 100644 --- a/aleksis/apps/alsijil/models.py +++ b/aleksis/apps/alsijil/models.py @@ -1,7 +1,6 @@ from datetime import date, datetime, timezone from typing import Optional, Union from urllib.parse import urlparse -import json from django.db import models from django.db.models import QuerySet @@ -536,10 +535,12 @@ class Documentation(CalendarEvent): "not_amended": True, } if obj_type is not None and obj_id is not None: - event_params.update({ - "type": obj_type, - "id": obj_id, - }) + event_params.update( + { + "type": obj_type, + "id": obj_id, + } + ) events = LessonEvent.get_single_events( date_start, diff --git a/aleksis/apps/alsijil/rules.py b/aleksis/apps/alsijil/rules.py index 9e39fb2760320ede8f98f9096326846ebec2c7d1..e3d9e43a6ac713428ce6882dc000c2c56a235ee5 100644 --- a/aleksis/apps/alsijil/rules.py +++ b/aleksis/apps/alsijil/rules.py @@ -16,6 +16,8 @@ from .util.predicates import ( has_lesson_group_object_perm, has_person_group_object_perm, has_personal_note_group_perm, + is_course_group_owner, + is_course_member, is_course_teacher, is_group_member, is_group_owner, @@ -363,12 +365,18 @@ view_documentation_predicate = has_person & ( add_perm("alsijil.view_documentation_rule", view_documentation_predicate) view_documentations_for_course_predicate = has_person & ( - has_global_perm("alsijil.view_documentation") | is_course_teacher + has_global_perm("alsijil.view_documentation") + | is_course_teacher + | is_course_member + | is_course_group_owner ) add_perm("alsijil.view_documentations_for_course_rule", view_documentations_for_course_predicate) view_documentations_for_group_predicate = has_person & ( - has_global_perm("alsijil.view_documentation") | is_group_owner + has_global_perm("alsijil.view_documentation") + | is_group_owner + | is_group_member + | is_parent_group_owner ) add_perm("alsijil.view_documentations_for_group_rule", view_documentations_for_group_predicate) diff --git a/aleksis/apps/alsijil/schema/__init__.py b/aleksis/apps/alsijil/schema/__init__.py index 1cfc2ca6526dc921a6d349c39c950fc0fc0cc664..037bf93c3bbf5e9e081d95accb3f56469d2ededf 100644 --- a/aleksis/apps/alsijil/schema/__init__.py +++ b/aleksis/apps/alsijil/schema/__init__.py @@ -1,7 +1,8 @@ -from django.db.models.query_utils import Q +from datetime import datetime + from django.core.exceptions import PermissionDenied +from django.db.models.query_utils import Q -from datetime import datetime import graphene from aleksis.apps.cursus.models import Course @@ -11,9 +12,9 @@ from aleksis.core.schema.base import FilterOrderList from ..models import Documentation from .documentation import ( DocumentationBatchCreateMutation, + DocumentationBatchCreateOrUpdateMutation, DocumentationBatchPatchMutation, DocumentationCreateMutation, - DocumentationBatchCreateOrUpdateMutation, DocumentationDeleteMutation, DocumentationType, ) diff --git a/aleksis/apps/alsijil/schema/documentation.py b/aleksis/apps/alsijil/schema/documentation.py index b72a7f50ffd3fd08fe5e013e7f3ac4687089e652..a2027c54d68deef8b765f3d1d381ba5fbc7f1858 100644 --- a/aleksis/apps/alsijil/schema/documentation.py +++ b/aleksis/apps/alsijil/schema/documentation.py @@ -1,4 +1,7 @@ from datetime import datetime, timezone + +from django.core.exceptions import PermissionDenied + import graphene from graphene_django.types import DjangoObjectType from graphene_django_cud.mutations import ( @@ -8,8 +11,6 @@ from graphene_django_cud.mutations import ( ) from guardian.shortcuts import get_objects_for_user -from django.core.exceptions import PermissionDenied - from aleksis.apps.chronos.models import LessonEvent from aleksis.core.schema.base import ( DeleteMutation, @@ -17,6 +18,7 @@ from aleksis.core.schema.base import ( PermissionBatchPatchMixin, PermissionsTypeMixin, ) + from ..models import Documentation @@ -137,11 +139,12 @@ class DocumentationBatchCreateOrUpdateMutation(graphene.Mutation): @classmethod def create_or_update(cls, info, doc): - id = doc.id + _id = doc.id - # Sadly, we can't use the update_or_create method since create_defaults is only introduced in Django 5.0 - if id.startswith("DUMMY"): - dummy, lesson_event_id, datetime_start, datetime_end = id.split(";") + # Sadly, we can't use the update_or_create method since create_defaults + # is only introduced in Django 5.0 + if _id.startswith("DUMMY"): + dummy, lesson_event_id, datetime_start, datetime_end = _id.split(";") lesson_event = LessonEvent.objects.get(id=lesson_event_id) if not info.context.user.has_perm( @@ -149,7 +152,8 @@ class DocumentationBatchCreateOrUpdateMutation(graphene.Mutation): ): raise PermissionDenied() - # Timezone removal is necessary due to ISO style offsets are no valid timezones. Instead, we take the timezone from the lesson_event and save it in a dedicated field. + # Timezone removal is necessary due to ISO style offsets are no valid timezones. + # Instead, we take the timezone from the lesson_event and save it in a dedicated field. return Documentation.objects.create( datetime_start=datetime.fromisoformat(datetime_start).replace(tzinfo=timezone.utc), datetime_end=datetime.fromisoformat(datetime_end).replace(tzinfo=timezone.utc), @@ -162,7 +166,7 @@ class DocumentationBatchCreateOrUpdateMutation(graphene.Mutation): group_note=doc.group_note or "", ) else: - obj = Documentation.objects.get(id=id) + obj = Documentation.objects.get(id=_id) if not info.context.user.has_perm("alsijil.edit_documentation_rule", obj): raise PermissionDenied() @@ -178,7 +182,7 @@ class DocumentationBatchCreateOrUpdateMutation(graphene.Mutation): return obj @classmethod - def mutate(cls, root, info, input): + def mutate(cls, root, info, input): # noqa objs = [cls.create_or_update(info, doc) for doc in input] return DocumentationBatchCreateOrUpdateMutation(documentations=objs) diff --git a/aleksis/apps/alsijil/util/predicates.py b/aleksis/apps/alsijil/util/predicates.py index d984fafc043be23dfebb13692209a50a4883d690..615142a890c1b0abcd360637e26a58917a3c2970 100644 --- a/aleksis/apps/alsijil/util/predicates.py +++ b/aleksis/apps/alsijil/util/predicates.py @@ -312,7 +312,7 @@ def is_lesson_event_teacher(user: User, obj: LessonEvent): or a teacher of the course, if the lesson event has one. """ if obj: - return obj.course and is_course_teacher(user, obj) or user.person in obj.all_teachers + return obj.course and is_course_teacher(user, obj.course) or user.person in obj.all_teachers return False @@ -329,19 +329,57 @@ def is_course_member(user: User, obj: Course): return False +@predicate +def is_course_group_owner(user: User, obj: Course): + """Predicate for group owners of a course. + + Checks whether the person linked to the user is a owner of any group + (or their respective parent groups) linked to the course. + """ + if obj: + for g in obj.groups.all(): + if user.person in g.owners.all(): + return True + for pg in g.parent_groups.all(): + if user.person in pg.owners.all(): + return True + return False + + @predicate def is_lesson_event_member(user: User, obj: LessonEvent): """Predicate for members of a lesson event. - Checks whether the person linked to the user is a members in the lesson event, + Checks whether the person linked to the user is a member in the lesson event, or a members of the course, if the lesson event has one. """ if obj: - if obj.course and is_course_member(user, obj): + if obj.course and is_course_member(user, obj.course): return True for g in obj.groups.all(): if user.person in g.members.all(): return True + + return False + + +@predicate +def is_lesson_event_group_owner(user: User, obj: LessonEvent): + """Predicate for group owners of a lesson event. + + Checks whether the person linked to the user is a owner of any group + (or their respective parent groups) linked to the lesson event, + or a owner of any group linked to the course, if the lesson event has one. + """ + if obj: + if obj.course and is_course_group_owner(user, obj.course): + return True + for g in obj.groups.all(): + if user.person in g.owners.all(): + return True + for pg in g.parent_groups.all(): + if user.person in pg.owners.all(): + return True return False @@ -350,10 +388,16 @@ def can_view_documentation(user: User, obj: Documentation): """Predicate which checks if the user is allowed to view a documentation.""" if obj: if obj.course: - return is_course_teacher(user, obj.course) | is_course_member(user, obj.course) + return ( + is_course_teacher(user, obj.course) + | is_course_member(user, obj.course) + | is_course_group_owner(user, obj.course) + ) if obj.lesson_event: - return is_lesson_event_teacher(user, obj.course) | is_lesson_event_member( - user, obj.course + return ( + is_lesson_event_teacher(user, obj.lesson_event) + | is_lesson_event_member(user, obj.lesson_event) + | is_lesson_event_group_owner(user, obj.lesson_event) ) return False @@ -363,7 +407,9 @@ def can_edit_documentation(user: User, obj: Documentation): """Predicate which checks if the user is allowed to edit or delete a documentation.""" if obj: if obj.course: - return is_course_teacher(user, obj.course) + return is_course_teacher(user, obj.course) | is_course_group_owner(user, obj.course) if obj.lesson_event: - return is_lesson_event_teacher(user, obj.course) + return is_lesson_event_teacher(user, obj.lesson_event) | is_lesson_event_group_owner( + user, obj.lesson_event + ) return False