Skip to content
Snippets Groups Projects
Jonathan Weth's avatar
Jonathan Weth authored
diff --git a/aleksis/apps/kolego/frontend/components/widgets/PlannedAbsencesForPersonWidget.vue b/aleksis/apps/kolego/frontend/components/widgets/PlannedAbsencesForPersonWidget.vue
new file mode 100644
index 0000000..14fd1d9
--- /dev/null
+++ b/aleksis/apps/kolego/frontend/components/widgets/PlannedAbsencesForPersonWidget.vue
@@ -0,0 +1,97 @@
+<template>
+  <v-card>
+    <v-card-title>
+      {{ $t("kolego.widgets.planned_absences.title") }}
+    </v-card-title>
+    <c-r-u-d-iterator
+      i18n-key="alsijil.coursebook.statistics"
+      :gql-query="gqlQuery"
+      :gql-additional-query-args="gqlQueryArgs"
+      :enable-create="false"
+      :enable-edit="false"
+      :enable-search="false"
+      :items-per-page="-1"
+      :elevated="false"
+    >
+      <template #default="{ items }">
+        <v-list>
+          <v-list-item v-for="item in items" :key="item.id">
+            <v-list-item-content>
+              <v-list-item-title>
+                <template
+                  v-if="
+                    $parseISODate(item.datetimeStart).hasSame(
+                      $parseISODate(item.datetimeEnd),
+                      'day',
+                    )
+                  "
+                >
+                  <time :datetime="item.datetimeStart" class="text-no-wrap">
+                    {{ $d($parseISODate(item.datetimeStart), "short") }},
+                  </time>
+
+                  <time :datetime="item.datetimeStart" class="text-no-wrap">
+                    {{ $d($parseISODate(item.datetimeStart), "shortTime") }}
+                  </time>
+                  <span>-</span>
+                  <time :datetime="item.datetimeEnd" class="text-no-wrap">
+                    {{ $d($parseISODate(item.datetimeEnd), "shortTime") }}
+                  </time>
+                </template>
+                <template v-else>
+                  <time :datetime="item.datetimeStart" class="text-no-wrap">
+                    {{ $d($parseISODate(item.datetimeStart), "shortDateTime") }}
+                  </time>
+                  <span>-</span>
+                  <time :datetime="item.datetimeEnd" class="text-no-wrap">
+                    {{ $d($parseISODate(item.datetimeEnd), "shortDateTime") }}
+                  </time>
+                </template>
+
+                <absence-reason-chip
+                  :absence-reason="item.reason"
+                  class="float-right"
+                  small
+                />
+              </v-list-item-title>
+              <v-list-item-subtitle>
+                {{ item.comment }}
+              </v-list-item-subtitle>
+            </v-list-item-content>
+            <v-list-item-icon>
+              <v-btn icon color="red"
+                ><v-icon>mdi-delete-outline</v-icon></v-btn
+              >
+            </v-list-item-icon>
+          </v-list-item>
+        </v-list>
+      </template>
+    </c-r-u-d-iterator>
+  </v-card>
+</template>
+
+<script>
+import personOverviewCardMixin from "aleksis.core/mixins/personOverviewCardMixin.js";
+import { absences } from "./absences.graphql";
+import CRUDIterator from "aleksis.core/components/generic/CRUDIterator.vue";
+import AbsenceReasonChip from "../AbsenceReasonChip.vue";
+export default {
+  name: "PlannedAbsencesForPersonWidget",
+  mixins: [personOverviewCardMixin],
+  components: { CRUDIterator, AbsenceReasonChip },
+  data() {
+    return {
+      gqlQuery: absences,
+    };
+  },
+  computed: {
+    gqlQueryArgs() {
+      return {
+        person: this.person.id,
+      };
+    },
+  },
+};
+</script>
+
+<style scoped></style>
diff --git a/aleksis/apps/kolego/frontend/components/widgets/absences.graphql b/aleksis/apps/kolego/frontend/components/widgets/absences.graphql
new file mode 100644
index 0000000..652648d
--- /dev/null
+++ b/aleksis/apps/kolego/frontend/components/widgets/absences.graphql
@@ -0,0 +1,27 @@
+query absences($orderBy: [String], $filters: JSONString, $person: ID!) {
+  items: plannedAbsencesForPerson(
+    orderBy: $orderBy
+    filters: $filters
+    person: $person
+  ) {
+    id
+    person {
+      id
+      fullName
+    }
+    reason {
+      id
+      shortName
+      name
+      colour
+      default
+    }
+    comment
+    datetimeStart
+    datetimeEnd
+    dateStart
+    dateEnd
+    canEdit
+    canDelete
+  }
+}
diff --git a/aleksis/apps/kolego/frontend/index.js b/aleksis/apps/kolego/frontend/index.js
index 260b3d9..aae17e7 100644
--- a/aleksis/apps/kolego/frontend/index.js
+++ b/aleksis/apps/kolego/frontend/index.js
@@ -1,3 +1,19 @@
+export const collectionItems = {
+  corePersonWidgets: [
+    {
+      key: "core-person-widgets",
+      component: () =>
+        import("./components/widgets/PlannedAbsencesForPersonWidget.vue"),
+      shouldDisplay: () => true,
+      colProps: {
+        cols: 12,
+        md: 6,
+        lg: 4,
+      },
+    },
+  ],
+};
+
 export default {
   meta: {
     inMenu: true,
diff --git a/aleksis/apps/kolego/preferences.py b/aleksis/apps/kolego/preferences.py
new file mode 100644
index 0000000..b8748c0
--- /dev/null
+++ b/aleksis/apps/kolego/preferences.py
@@ -0,0 +1,24 @@
+from django.utils.translation import gettext_lazy as _
+
+from dynamic_preferences.preferences import Section
+from dynamic_preferences.types import (
+    ModelMultipleChoicePreference,
+)
+
+from aleksis.core.models import GroupType
+from aleksis.core.registries import site_preferences_registry
+
+kolego = Section("kolego", verbose_name=_("Absences"))
+
+
+@site_preferences_registry.register
+class GroupTypesViewPersonAbsences(ModelMultipleChoicePreference):
+    section = kolego
+    name = "group_types_view_person_absences"
+    required = False
+    default = []
+    model = GroupType
+    verbose_name = _(
+        "User is allowed to view (planned) absences for members "
+        "of groups the user is an owner of with these group types"
+    )
diff --git a/aleksis/apps/kolego/rules.py b/aleksis/apps/kolego/rules.py
index 535b0f3..07cfd49 100644
--- a/aleksis/apps/kolego/rules.py
+++ b/aleksis/apps/kolego/rules.py
@@ -8,6 +8,8 @@ from aleksis.core.util.predicates import (
     has_person,
 )

+from .util.predicates import can_view_absences_for_person
+
 view_absences_predicate = has_person & (
     has_global_perm("kolego.view_absence") | has_any_object("kolego.view_absence", Absence)
 )
@@ -89,3 +91,8 @@ view_menu_predicate = has_person & (
     view_absences_predicate | view_absencereasons_predicate | view_absencereasontags_predicate
 )
 rules.add_perm("kolego.view_menu_rule", view_menu_predicate)
+
+view_person_absences_predicate = has_person & (
+    has_global_perm("kolego.view_absence") | can_view_absences_for_person
+)
+rules.add_perm("kolego.view_person_absences_rule", view_person_absences_predicate)
diff --git a/aleksis/apps/kolego/schema/__init__.py b/aleksis/apps/kolego/schema/__init__.py
index bf93f42..961c4a6 100644
--- a/aleksis/apps/kolego/schema/__init__.py
+++ b/aleksis/apps/kolego/schema/__init__.py
@@ -1,4 +1,5 @@
 from django.apps import apps
+from django.utils import timezone
 from django.db.models import QuerySet

 import graphene
@@ -6,6 +7,7 @@ import graphene_django_optimizer
 from guardian.shortcuts import get_objects_for_user

 from aleksis.apps.kolego.models.absence import Absence, AbsenceReason, AbsenceReasonTag
+from aleksis.core.models import Person
 from aleksis.core.schema.base import FilterOrderList
 from aleksis.core.util.core_helpers import filter_active_school_term_by_date

@@ -28,6 +30,7 @@ from .absence import (
 class Query(graphene.ObjectType):
     app_name = graphene.String()
     absences = FilterOrderList(AbsenceType)
+    planned_absences_for_person = FilterOrderList(AbsenceType, person=graphene.ID(required=True))
     absence_reasons = FilterOrderList(AbsenceReasonType)
     absence_reason_tags = FilterOrderList(AbsenceReasonTagType)
     all_absence_reason_tags = FilterOrderList(AbsenceReasonTagType)
@@ -47,6 +50,21 @@ class Query(graphene.ObjectType):
             info,
         )

+    @staticmethod
+    def resolve_planned_absences_for_person(root, info, person: str, **kwargs):
+        person = Person.objects.get(pk=person)
+        if not info.context.user.has_perm("kolego.view_person_absences_rule", person):
+            return []
+        return graphene_django_optimizer.query(
+            filter_active_school_term_by_date(
+                info.context,
+                Absence.objects.filter(person=person, datetime_end__gte=timezone.now()).order_by(
+                    "datetime_start"
+                ),
+            ),
+            info,
+        )
+
     @staticmethod
     def resolve_absencereasons(root, info, **kwargs) -> QuerySet:
         if not info.context.user.has_perm("kolego.fetch_absencereasons_rule"):
diff --git a/aleksis/apps/kolego/util/predicates.py b/aleksis/apps/kolego/util/predicates.py
new file mode 100644
index 0000000..7db1702
--- /dev/null
+++ b/aleksis/apps/kolego/util/predicates.py
@@ -0,0 +1,16 @@
+from django.contrib.auth.models import User
+
+from rules import predicate
+
+from aleksis.core.models import Person
+from aleksis.core.util.core_helpers import get_site_preferences
+
+
+@predicate
+def can_view_absences_for_person(user: User, obj: Person) -> bool:
+    """Predicate for viewing absences of a person."""
+    group_types = get_site_preferences()["alsijil__group_types_view_person_absences"]
+    qs = obj.member_of.filter(owners=user.person)
+    if not group_types.exists():
+        return False
+    return qs.filter(group_type__in=group_types).exists()
da41fc7e
History