Skip to content
Snippets Groups Projects
  • Jonathan Weth's avatar
    da41fc7e
    Add first try of a planned absences widget · da41fc7e
    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
    Add first try of a planned absences widget
    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()