Skip to content
Snippets Groups Projects
Commit e934ccef authored by Jonathan Weth's avatar Jonathan Weth :keyboard:
Browse files

Merge branch '295-respect-permissions-in-coursebook-frontend' into 'master'

Resolve "Respect permissions in coursebook frontend"

Closes #295

See merge request !398
parents 9f2e8cd3 4e5f6669
No related branches found
No related tags found
1 merge request!398Resolve "Respect permissions in coursebook frontend"
Pipeline #192449 failed
Showing
with 409 additions and 108 deletions
......@@ -15,6 +15,7 @@
<lesson-notes class="span-2" v-bind="documentationPartProps" />
<participation-list
v-if="documentation.canEditParticipationStatus"
:include-present="false"
class="participation-list"
v-bind="documentationPartProps"
......
......@@ -14,6 +14,7 @@ import {
} from "./participationStatus.graphql";
import SlideIterator from "aleksis.core/components/generic/SlideIterator.vue";
import PersonalNotes from "../personal_notes/PersonalNotes.vue";
import PersonalNoteChip from "../personal_notes/PersonalNoteChip.vue";
import ExtraMarkChip from "../../extra_marks/ExtraMarkChip.vue";
import TardinessChip from "./TardinessChip.vue";
import TardinessField from "./TardinessField.vue";
......@@ -31,6 +32,7 @@ export default {
AbsenceReasonGroupSelect,
AbsenceReasonButtons,
PersonalNotes,
PersonalNoteChip,
LessonInformation,
MessageBox,
MobileFullscreenDialog,
......@@ -255,18 +257,12 @@ export default {
small
:absence-reason="item.absenceReason"
/>
<v-chip
<personal-note-chip
v-for="note in item.notesWithNote"
:key="'text-note-note-overview-' + note.id"
:note="note"
small
>
<v-avatar left>
<v-icon small>mdi-note-outline</v-icon>
</v-avatar>
<span class="text-truncate" style="max-width: 30ch">
{{ note.note }}
</span>
</v-chip>
/>
<extra-mark-chip
v-for="note in item.notesWithExtraMark"
:key="'extra-mark-note-overview-' + note.id"
......
......@@ -57,6 +57,17 @@ export default {
);
},
},
computed: {
showLabel() {
return !!this.labelKey || !this.canOpenParticipation;
},
innerLabelKey() {
if (this.documentation.futureNoticeParticipationStatus) {
return "alsijil.coursebook.notes.future";
}
return this.labelKey;
},
},
};
</script>
......@@ -77,9 +88,9 @@ export default {
v-on="on"
@click="touchDocumentation"
>
<v-icon :left="!!labelKey">mdi-account-edit-outline</v-icon>
<template v-if="labelKey">
{{ $t(labelKey) }}
<v-icon :left="showLabel">mdi-account-edit-outline</v-icon>
<template v-if="showLabel">
{{ $t(innerLabelKey) }}
</template>
</v-chip>
</template>
......
......@@ -112,6 +112,9 @@ query documentationsForCoursebook(
canEdit
futureNotice
canDelete
futureNoticeParticipationStatus
canEditParticipationStatus
canViewParticipationStatus
}
}
......
......@@ -100,6 +100,6 @@ export default {
gap: 1em;
}
.vertical {
grid-template-columns: 1fr;
grid-template-columns: minmax(0, 1fr);
}
</style>
<script setup>
import AbsenceReasonChip from "aleksis.apps.kolego/components/AbsenceReasonChip.vue";
import ExtraMarkChip from "../../extra_marks/ExtraMarkChip.vue";
import ExtraMarksNote from "../personal_notes/ExtraMarksNote.vue";
import TardinessChip from "../absences/TardinessChip.vue";
import PersonalNoteChip from "../personal_notes/PersonalNoteChip.vue";
import TextNoteCard from "../personal_notes/TextNoteCard.vue";
</script>
<template>
<div
class="d-flex align-center justify-space-between justify-md-end flex-wrap gap"
>
<v-chip dense color="success" outlined v-if="total > 0">
{{ $t("alsijil.coursebook.present_number", { present, total }) }}
</v-chip>
<absence-reason-chip
v-for="[reasonId, participations] in Object.entries(absences)"
:key="'reason-' + reasonId"
:absence-reason="participations[0].absenceReason"
dense
<div>
<div
class="d-flex align-center justify-space-between justify-md-end flex-wrap gap"
v-if="compact || documentation.canViewParticipationStatus"
>
<template #append>
<span
>:
<span>
{{
participations
.slice(0, 5)
.map((participation) => participation.person.firstName)
.join(", ")
}}
</span>
<span v-if="participations.length > 5">
<!-- eslint-disable @intlify/vue-i18n/no-raw-text -->
+{{ participations.length - 5 }}
<!-- eslint-enable @intlify/vue-i18n/no-raw-text -->
</span>
</span>
<v-chip
dense
color="success"
outlined
v-if="total > 0 && documentation.canViewParticipationStatus"
>
{{
$t("alsijil.coursebook.participations.present_number", {
present,
total,
})
}}
</v-chip>
<v-chip
dense
color="success"
outlined
@click="$emit('open')"
v-bind="dialogActivator.attrs"
v-on="dialogActivator.on"
v-else-if="
total == 1 &&
present == 1 &&
!documentation.canViewParticipationStatus
"
>
{{ $t("alsijil.coursebook.participations.present") }}
</v-chip>
<template v-if="documentation.canViewParticipationStatus">
<absence-reason-chip
v-for="[reasonId, participations] in Object.entries(absences)"
:key="'reason-' + reasonId"
:absence-reason="participations[0].absenceReason"
dense
>
<template #append>
<span
>:
<span>
{{
participations
.slice(0, 5)
.map((participation) => participation.person.firstName)
.join(", ")
}}
</span>
<span v-if="participations.length > 5">
<!-- eslint-disable @intlify/vue-i18n/no-raw-text -->
+{{ participations.length - 5 }}
<!-- eslint-enable @intlify/vue-i18n/no-raw-text -->
</span>
</span>
</template>
</absence-reason-chip>
</template>
<template v-else>
<absence-reason-chip
v-for="[reasonId, participations] in Object.entries(absences)"
:key="'reason-' + reasonId"
:absence-reason="participations[0].absenceReason"
dense
@click="$emit('open')"
v-bind="dialogActivator.attrs"
v-on="dialogActivator.on"
/>
</template>
</absence-reason-chip>
<extra-mark-chip
v-for="[markId, [mark, ...participations]] in Object.entries(
extraMarkChips,
)"
:key="'extra-mark-' + markId"
:extra-mark="mark"
dense
>
<template #append>
<span
>:
<span>
{{
participations
.slice(0, 5)
.map((participation) => participation.person.firstName)
.join(", ")
}}
</span>
<span v-if="participations.length > 5">
<!-- eslint-disable @intlify/vue-i18n/no-raw-text -->
+{{ participations.length - 5 }}
<!-- eslint-enable @intlify/vue-i18n/no-raw-text -->
</span>
</span>
<template v-if="documentation.canViewParticipationStatus">
<extra-mark-chip
v-for="[markId, [mark, ...participations]] in Object.entries(
extraMarkChips,
)"
:key="'extra-mark-' + markId"
:extra-mark="mark"
dense
>
<template #append>
<span
>:
<span>
{{
participations
.slice(0, 5)
.map((participation) => participation.person.firstName)
.join(", ")
}}
</span>
<span v-if="participations.length > 5">
<!-- eslint-disable @intlify/vue-i18n/no-raw-text -->
+{{ participations.length - 5 }}
<!-- eslint-enable @intlify/vue-i18n/no-raw-text -->
</span>
</span>
</template>
</extra-mark-chip>
</template>
<template v-else>
<extra-mark-chip
v-for="[markId, [mark, ...participations]] in Object.entries(
extraMarkChips,
)"
:key="'extra-mark-' + markId"
:extra-mark="mark"
dense
@click="$emit('open')"
v-bind="dialogActivator.attrs"
v-on="dialogActivator.on"
/>
</template>
</extra-mark-chip>
<tardiness-chip v-if="tardyParticipations.length > 0">
{{ $t("alsijil.personal_notes.late") }}
<template v-if="documentation.canViewParticipationStatus">
<tardiness-chip v-if="tardyParticipations.length > 0">
<template #default>
{{ $t("alsijil.personal_notes.late") }}
</template>
<template #append>
<span
>:
{{
tardyParticipations
.slice(0, 5)
.map((participation) => participation.person.firstName)
.join(", ")
}}
<template #append>
<span
>:
{{
tardyParticipations
.slice(0, 5)
.map((participation) => participation.person.firstName)
.join(", ")
}}
<span v-if="tardyParticipations.length > 5">
<!-- eslint-disable @intlify/vue-i18n/no-raw-text -->
+{{ tardyParticipations.length - 5 }}
<!-- eslint-enable @intlify/vue-i18n/no-raw-text -->
</span>
</span>
</template>
</tardiness-chip>
</template>
<template v-else>
<tardiness-chip
v-if="tardyParticipations.length > 0"
:tardiness="
tardyParticipations.length == 1
? tardyParticipations[0].tardiness
: undefined
"
@click="$emit('open')"
v-bind="dialogActivator.attrs"
v-on="dialogActivator.on"
/>
</template>
<span v-if="tardyParticipations.length > 5">
<!-- eslint-disable @intlify/vue-i18n/no-raw-text -->
+{{ tardyParticipations.length - 5 }}
<!-- eslint-enable @intlify/vue-i18n/no-raw-text -->
</span>
</span>
<template v-if="!documentation.canViewParticipationStatus && total == 1">
<personal-note-chip
v-for="note in documentation?.participations[0]?.notesWithNote"
:key="'text-note-note-' + note.id"
:note="note"
@click="$emit('open')"
v-bind="dialogActivator.attrs"
v-on="dialogActivator.on"
/>
</template>
</tardiness-chip>
<manage-students-trigger
:label-key="total == 0 ? 'alsijil.coursebook.notes.show_list' : ''"
v-bind="documentationPartProps"
/>
<manage-students-trigger
v-if="documentation.canEditParticipationStatus"
:label-key="manageStudentsLabelKey"
v-bind="documentationPartProps"
/>
</div>
<!-- not compact -->
<div class="main-body" v-else>
<template
v-if="
tardyParticipations.length > 0 || Object.entries(absences).length > 0
"
>
<v-divider />
<div
class="d-flex align-center justify-space-between justify-md-end flex-wrap gap"
>
<tardiness-chip
v-if="tardyParticipations.length > 0"
:tardiness="
tardyParticipations.length == 1
? tardyParticipations[0].tardiness
: undefined
"
/>
<absence-reason-chip
v-for="[reasonId, participations] in Object.entries(absences)"
:key="'reason-' + reasonId"
:absence-reason="participations[0].absenceReason"
dense
/>
</div>
</template>
<template v-if="total == 1">
<v-divider />
<extra-marks-note
v-bind="documentationPartProps"
:participation="documentation?.participations[0]"
:value="documentation?.participations[0].notesWithExtraMark"
:disabled="true"
/>
</template>
<template
v-if="
total == 1 &&
documentation?.participations[0]?.notesWithNote.length > 0
"
>
<v-divider />
<div>
<text-note-card
v-for="note in documentation?.participations[0]?.notesWithNote"
:key="'text-note-note-' + note.id"
:note="note"
/>
</div>
</template>
</div>
</div>
</template>
......@@ -161,6 +303,12 @@ export default {
tardyParticipations() {
return this.documentation.participations.filter((p) => p.tardiness);
},
manageStudentsLabelKey() {
if (this.total == 0) {
return "alsijil.coursebook.notes.show_list";
}
return "";
},
},
};
</script>
......@@ -169,4 +317,9 @@ export default {
.gap {
gap: 0.25em;
}
.main-body {
display: grid;
grid-template-columns: minmax(0, 1fr);
gap: 1em;
}
</style>
......@@ -88,7 +88,7 @@ export default {
:label="value.name"
:value="value.id"
v-model="model"
:disabled="loading"
:disabled="$attrs?.disabled || loading"
:true-value="true"
:false-value="false"
>
......
......@@ -20,7 +20,7 @@ export default {
<extra-mark-note-checkbox
v-for="extraMark in extraMarks"
:key="'checkbox-extramark-' + extraMark.id"
v-bind="personalNoteRelatedProps"
v-bind="{ ...personalNoteRelatedProps, ...$attrs }"
:value="extraMark"
:personal-note="value.find((pn) => pn.extraMark.id === extraMark.id)"
/>
......
<script>
export default {
name: "PersonalNoteChip",
props: {
note: {
type: Object,
required: true,
},
loading: {
type: Boolean,
required: false,
default: false,
},
},
extends: "v-chip",
};
</script>
<template>
<v-tooltip bottom>
<template #activator="{ on, attrs }">
<v-chip
dense
outlined
v-bind="{ ...$attrs, ...attrs }"
v-on="{ ...$listeners, ...on }"
>
<v-avatar left>
<v-icon small>mdi-note-outline</v-icon>
</v-avatar>
<slot name="prepend" />
<slot>
<span class="text-truncate" style="max-width: 30ch">
{{ note.note }}
</span>
</slot>
<slot name="append" />
<v-avatar right v-if="loading">
<v-progress-circular indeterminate :size="16" :width="2" />
</v-avatar>
</v-chip>
</template>
<span v-text="note.note" />
</v-tooltip>
</template>
<template>
<v-card
outlined
dense
rounded="lg"
class="mb-2"
v-bind="$attrs"
v-on="$listeners"
>
<v-card-title class="text-subtitle-2 pb-1 font-weight-medium">
{{ $t("alsijil.personal_notes.card.title") }}
</v-card-title>
<v-card-text>{{ note.note || "" }}</v-card-text>
</v-card>
</template>
<script>
export default {
name: "TextNoteCard",
props: {
note: {
type: Object,
required: true,
},
},
};
</script>
......@@ -77,7 +77,8 @@
}
},
"notes": {
"show_list": "List of participants"
"show_list": "List of participants",
"future": "Lesson is in the future"
},
"notices": {
"future": "Editing this lesson isn't allowed as this lesson is in the future.",
......@@ -100,9 +101,12 @@
"field": "Edit subject"
}
},
"present_number": "{present}/{total} present",
"no_data": "No lessons for the selected groups and courses in this period",
"no_results": "No search results for {search}",
"participations": {
"present_number": "{present}/{total} present",
"present": "Present"
},
"absences": {
"action_for_selected": "Mark selected participant as: | Mark {count} selected participants as",
"title": "Register absences",
......@@ -119,6 +123,9 @@
}
},
"personal_notes": {
"card": {
"title": "Personal note"
},
"note": "Note",
"create_personal_note": "Add another note",
"tardiness": "Tardiness",
......
......@@ -535,6 +535,7 @@ class Documentation(CalendarEvent):
events: list,
incomplete: Optional[bool] = False,
absences_exist: Optional[bool] = False,
request: Optional[HttpRequest] = None,
) -> tuple:
"""Get all the documentations for the events.
Create dummy documentations if none exist.
......@@ -566,11 +567,22 @@ class Documentation(CalendarEvent):
doc = next(existing_documentations_event, None)
if doc:
if (incomplete and doc.topic) or (
absences_exist
and (
not doc.participations.all()
or not [d for d in doc.participations.all() if d.absence_reason]
if (
(incomplete and doc.topic)
or (
not request.user.has_perm(
"alsijil.edit_participation_status_for_documentation_rule", doc
)
and not doc.participations.filter(
person__pk=request.user.person.pk, absence_reason__isnull=False
).exists()
)
or (
absences_exist
and (
not doc.participations.all()
or not [d for d in doc.participations.all() if d.absence_reason]
)
)
):
continue
......@@ -609,6 +621,7 @@ class Documentation(CalendarEvent):
start: datetime,
end: datetime,
incomplete: Optional[bool] = False,
request: Optional[HttpRequest] = None,
) -> tuple:
"""Get all the documentations for the person from start to end datetime.
Create dummy documentations if none exist.
......@@ -627,7 +640,7 @@ class Documentation(CalendarEvent):
with_reference_object=True,
)
return Documentation.get_documentations_for_events(start, end, events, incomplete)
return Documentation.get_documentations_for_events(start, end, events, incomplete, request)
@classmethod
def parse_dummy(
......
......@@ -431,11 +431,19 @@ add_perm(
view_participation_status_for_documentation_predicate,
)
edit_participation_status_for_documentation_predicate = (
edit_participation_status_for_documentation_with_time_range_predicate = (
has_person
& (has_global_perm("alsijil.change_participationstatus") | can_edit_participation_status)
& is_in_allowed_time_range_for_participation_status
)
add_perm(
"alsijil.edit_participation_status_for_documentation_with_time_range_rule",
edit_participation_status_for_documentation_with_time_range_predicate,
)
edit_participation_status_for_documentation_predicate = has_person & (
has_global_perm("alsijil.change_participationstatus") | can_edit_participation_status
)
add_perm(
"alsijil.edit_participation_status_for_documentation_rule",
edit_participation_status_for_documentation_predicate,
......
......@@ -141,6 +141,7 @@ class Query(graphene.ObjectType):
events,
incomplete,
absences_exist,
info.context,
)
return docs + dummies
......@@ -218,6 +219,7 @@ class Query(graphene.ObjectType):
person,
start,
end,
info.context,
)
lessons_for_person.append(LessonsForPersonType(id=person, lessons=docs + dummies))
......
......@@ -4,7 +4,14 @@ import graphene
from graphene_django.types import DjangoObjectType
from reversion import create_revision, set_comment, set_user
from aleksis.apps.alsijil.util.predicates import can_edit_documentation, is_in_allowed_time_range
from aleksis.apps.alsijil.util.predicates import (
can_edit_documentation,
is_in_allowed_time_range,
is_in_allowed_time_range_for_participation_status,
)
from aleksis.apps.alsijil.util.predicates import (
can_edit_participation_status as can_edit_participation_status_predicate,
)
from aleksis.apps.chronos.schema import LessonEventType
from aleksis.apps.cursus.models import Subject
from aleksis.apps.cursus.schema import CourseType, SubjectType
......@@ -13,6 +20,7 @@ from aleksis.core.schema.base import (
DjangoFilterMixin,
PermissionsTypeMixin,
)
from aleksis.core.util.core_helpers import has_person
from ..models import Documentation
from .participation_status import ParticipationStatusType
......@@ -46,6 +54,10 @@ class DocumentationType(PermissionsTypeMixin, DjangoFilterMixin, DjangoObjectTyp
participations = graphene.List(ParticipationStatusType, required=False)
future_notice = graphene.Boolean(required=False)
future_notice_participation_status = graphene.Boolean(required=False)
can_edit_participation_status = graphene.Boolean(required=False)
can_view_participation_status = graphene.Boolean(required=False)
old_id = graphene.ID(required=False)
......@@ -68,15 +80,36 @@ class DocumentationType(PermissionsTypeMixin, DjangoFilterMixin, DjangoObjectTyp
)
@staticmethod
def resolve_participations(root: Documentation, info, **kwargs):
if not info.context.user.has_perm(
def resolve_future_notice_participation_status(root: Documentation, info, **kwargs):
"""Shows whether the user can edit all participation statuses based on the current time.
This checks whether the documentation is in the future.
"""
return not is_in_allowed_time_range_for_participation_status(info.context.user, root)
@staticmethod
def resolve_can_edit_participation_status(root: Documentation, info, **kwargs):
"""Shows whether the user can edit all participation statuses of the documentation"""
return can_edit_participation_status_predicate(info.context.user, root)
@staticmethod
def resolve_can_view_participation_status(root: Documentation, info, **kwargs):
"""Shows whether the user can view all participation statuses of the documentation"""
return info.context.user.has_perm(
"alsijil.view_participation_status_for_documentation_rule", root
):
return []
)
@staticmethod
def resolve_participations(root: Documentation, info, **kwargs):
# A dummy documentation will not have any participations
if str(root.pk).startswith("DUMMY") or not hasattr(root, "participations"):
return []
elif not info.context.user.has_perm(
"alsijil.view_participation_status_for_documentation_rule", root
):
if has_person(info.context.user):
return root.participations.filter(person=info.context.user.person)
return []
return root.participations.all()
......@@ -147,7 +180,8 @@ class TouchDocumentationMutation(graphene.Mutation):
)
if not info.context.user.has_perm(
"alsijil.edit_participation_status_for_documentation_rule", documentation
"alsijil.edit_participation_status_for_documentation_with_time_range_rule",
documentation,
):
raise PermissionDenied()
......
......@@ -73,7 +73,8 @@ class ParticipationStatusBatchPatchMutation(BaseBatchPatchMutation):
@classmethod
def after_update_obj(cls, root, info, input, obj, full_input): # noqa: A002
if not info.context.user.has_perm(
"alsijil.edit_participation_status_for_documentation_rule", obj.related_documentation
"alsijil.edit_participation_status_for_documentation_with_time_range_rule",
obj.related_documentation,
):
raise PermissionDenied()
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment