diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/Coursebook.vue b/aleksis/apps/alsijil/frontend/components/coursebook/Coursebook.vue
index 1403a38bb524c7fa407ba94f8193cbda00e8b6ba..7c1b9ac900da53ff7ab97a5b459e9be70891521f 100644
--- a/aleksis/apps/alsijil/frontend/components/coursebook/Coursebook.vue
+++ b/aleksis/apps/alsijil/frontend/components/coursebook/Coursebook.vue
@@ -1,68 +1,73 @@
 <template>
-  <c-r-u-d-iterator
-    i18n-key="alsijil.coursebook"
-    :gql-query="gqlQuery"
-    :gql-additional-query-args="gqlQueryArgs"
-    :enable-create="false"
-    :enable-edit="false"
-    :elevated="false"
-    @lastQuery="lastQuery = $event"
-    ref="iterator"
-    fixed-header
-    disable-pagination
-    hide-default-footer
-    use-deep-search
-  >
-    <template #additionalActions="{ attrs, on }">
-      <coursebook-filters v-model="filters" />
-    </template>
-    <template #default="{ items }">
-      <coursebook-loader />
-      <coursebook-day
-        v-for="{ date, docs, first, last } in groupDocsByDay(items)"
-        v-intersect="{
-          handler: intersectHandler(date, first, last),
-          options: {
-            rootMargin: '-' + topMargin + 'px 0px 0px 0px',
-            threshold: [0, 1],
-          },
-        }"
-        :date="date"
-        :docs="docs"
-        :lastQuery="lastQuery"
-        :focus-on-mount="initDate && initDate.toMillis() === date.toMillis()"
-        @init="transition"
-        :key="'day-' + date"
-        ref="days"
-        :extra-marks="extraMarks"
-      />
-      <coursebook-loader />
+  <div>
+    <c-r-u-d-iterator
+      i18n-key="alsijil.coursebook"
+      :gql-query="gqlQuery"
+      :gql-additional-query-args="gqlQueryArgs"
+      :enable-create="false"
+      :enable-edit="false"
+      :elevated="false"
+      @lastQuery="lastQuery = $event"
+      ref="iterator"
+      fixed-header
+      disable-pagination
+      hide-default-footer
+      use-deep-search
+    >
+      <template #additionalActions="{ attrs, on }">
+        <coursebook-filters v-model="filters" />
+      </template>
+      <template #default="{ items }">
+        <coursebook-loader />
+        <coursebook-day
+          v-for="{ date, docs, first, last } in groupDocsByDay(items)"
+          v-intersect="{
+            handler: intersectHandler(date, first, last),
+            options: {
+              rootMargin: '-' + topMargin + 'px 0px 0px 0px',
+              threshold: [0, 1],
+            },
+          }"
+          :date="date"
+          :docs="docs"
+          :lastQuery="lastQuery"
+          :focus-on-mount="initDate && initDate.toMillis() === date.toMillis()"
+          @init="transition"
+          :key="'day-' + date"
+          ref="days"
+          :extra-marks="extraMarks"
+        />
+        <coursebook-loader />
 
-      <date-select-footer
-        :value="currentDate"
-        @input="gotoDate"
-        @prev="gotoPrev"
-        @next="gotoNext"
-      />
-    </template>
-    <template #loading>
-      <coursebook-loader :number-of-days="10" :number-of-docs="5" />
-    </template>
+        <date-select-footer
+          :value="currentDate"
+          @input="gotoDate"
+          @prev="gotoPrev"
+          @next="gotoNext"
+        />
+      </template>
+      <template #loading>
+        <coursebook-loader :number-of-days="10" :number-of-docs="5" />
+      </template>
 
-    <template #no-data>
-      <CoursebookEmptyMessage icon="mdi-book-off-outline">
-        {{ $t("alsijil.coursebook.no_data") }}
-      </CoursebookEmptyMessage>
-    </template>
+      <template #no-data>
+        <CoursebookEmptyMessage icon="mdi-book-off-outline">
+          {{ $t("alsijil.coursebook.no_data") }}
+        </CoursebookEmptyMessage>
+      </template>
 
-    <template #no-results>
-      <CoursebookEmptyMessage icon="mdi-book-alert-outline">
-        {{
-          $t("alsijil.coursebook.no_results", { search: $refs.iterator.search })
-        }}
-      </CoursebookEmptyMessage>
-    </template>
-  </c-r-u-d-iterator>
+      <template #no-results>
+        <CoursebookEmptyMessage icon="mdi-book-alert-outline">
+          {{
+            $t("alsijil.coursebook.no_results", {
+              search: $refs.iterator.search,
+            })
+          }}
+        </CoursebookEmptyMessage>
+      </template>
+    </c-r-u-d-iterator>
+    <absence-creation-dialog />
+  </div>
 </template>
 
 <script>
@@ -76,6 +81,8 @@ import CoursebookLoader from "./CoursebookLoader.vue";
 import CoursebookEmptyMessage from "./CoursebookEmptyMessage.vue";
 import { extraMarks } from "../extra_marks/extra_marks.graphql";
 
+import AbsenceCreationDialog from "./absences/AbsenceCreationDialog.vue";
+
 export default {
   name: "Coursebook",
   components: {
@@ -85,6 +92,7 @@ export default {
     CRUDIterator,
     DateSelectFooter,
     CoursebookDay,
+    AbsenceCreationDialog,
   },
   props: {
     filterType: {
diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/absences/AbsenceCreationDialog.vue b/aleksis/apps/alsijil/frontend/components/coursebook/absences/AbsenceCreationDialog.vue
new file mode 100644
index 0000000000000000000000000000000000000000..dbb504c393b0dc03f43b647ca305f614fd7df1da
--- /dev/null
+++ b/aleksis/apps/alsijil/frontend/components/coursebook/absences/AbsenceCreationDialog.vue
@@ -0,0 +1,174 @@
+<template>
+  <mobile-fullscreen-dialog v-model="popup" persistent>
+    <template #activator="activator">
+      <fab-button
+        color="secondary"
+        @click="popup = true"
+        :disabled="popup"
+        :class="{
+          'd-none': !checkPermission('alsijil.view_register_absence_rule'),
+        }"
+        icon-text="$plus"
+        i18n-key="alsijil.coursebook.absences.button"
+      >
+        <v-icon>$plus</v-icon>
+      </fab-button>
+    </template>
+    <template #title>
+      <div>
+        {{ $t("alsijil.coursebook.absences.title") }}
+      </div>
+      <span v-if="!form" class="px-2">·</span>
+      <div v-if="!form">
+        {{ $t("alsijil.coursebook.absences.summary") }}
+      </div>
+    </template>
+    <template #content>
+      <absence-creation-form
+        :persons="persons"
+        :start-date="startDate"
+        :end-date="endDate"
+        :comment="comment"
+        :absence-reason="absenceReason"
+        @valid="formValid = $event"
+        @persons="persons = $event"
+        @start-date="startDate = $event"
+        @end-date="endDate = $event"
+        @comment="comment = $event"
+        @absence-reason="absenceReason = $event"
+        :class="{
+          'd-none': !form,
+        }"
+      />
+      <absence-creation-summary
+        v-if="!form"
+        :persons="persons"
+        :start-date="startDate"
+        :end-date="endDate"
+        @loading="handleLoading"
+      />
+    </template>
+    <template #actionsLeft>
+      <cancel-button @click="cancel" />
+    </template>
+    <template #actions>
+      <!-- secondary -->
+      <secondary-action-button
+        @click="form = true"
+        v-if="!form"
+        :disabled="loading"
+        i18n-key="actions.back"
+      >
+        <v-icon left>$prev</v-icon>
+        {{ $t("actions.back") }}
+      </secondary-action-button>
+      <!-- primary -->
+      <save-button
+        v-if="form"
+        @click="form = false"
+        :loading="loading"
+        :disabled="!formValid"
+      >
+        {{ $t("actions.continue") }}
+        <v-icon right>$next</v-icon>
+      </save-button>
+      <save-button
+        v-else
+        i18n-key="actions.confirm"
+        @click="confirm"
+        :loading="loading"
+      />
+    </template>
+  </mobile-fullscreen-dialog>
+</template>
+
+<script>
+import MobileFullscreenDialog from "aleksis.core/components/generic/dialogs/MobileFullscreenDialog.vue";
+import AbsenceCreationForm from "./AbsenceCreationForm.vue";
+import AbsenceCreationSummary from "./AbsenceCreationSummary.vue";
+import FabButton from "aleksis.core/components/generic/buttons/FabButton.vue";
+import CancelButton from "aleksis.core/components/generic/buttons/CancelButton.vue";
+import SaveButton from "aleksis.core/components/generic/buttons/SaveButton.vue";
+import SecondaryActionButton from "aleksis.core/components/generic/buttons/SecondaryActionButton.vue";
+import loadingMixin from "aleksis.core/mixins/loadingMixin.js";
+import permissionsMixin from "aleksis.core/mixins/permissions.js";
+import mutateMixin from "aleksis.core/mixins/mutateMixin.js";
+
+import { createAbsencesForPersons } from "./absenceCreation.graphql";
+
+export default {
+  name: "AbsenceCreationDialog",
+  components: {
+    MobileFullscreenDialog,
+    AbsenceCreationForm,
+    AbsenceCreationSummary,
+    CancelButton,
+    SaveButton,
+    SecondaryActionButton,
+    FabButton,
+  },
+  mixins: [loadingMixin, mutateMixin, permissionsMixin],
+  data() {
+    return {
+      popup: false,
+      form: true,
+      formValid: false,
+      persons: [],
+      startDate: "",
+      endDate: "",
+      comment: "",
+      absenceReason: "",
+    };
+  },
+  mounted() {
+    this.addPermissions(["alsijil.view_register_absence_rule"]);
+  },
+  methods: {
+    cancel() {
+      this.popup = false;
+      this.form = true;
+      this.clearForm();
+    },
+    clearForm() {
+      this.persons = [];
+      this.startDate = "";
+      this.endDate = "";
+      this.comment = "";
+      this.absenceReason = "";
+    },
+    confirm() {
+      this.handleLoading(true);
+      this.mutate(
+        createAbsencesForPersons,
+        {
+          persons: this.persons.map((p) => p.id),
+          start: this.startDate,
+          end: this.endDate,
+          comment: this.comment,
+          reason: this.absenceReason,
+        },
+        (storedDocumentations, incomingStatuses) => {
+          const documentation = storedDocumentations.find(
+            (doc) => doc.id === this.documentation.id,
+          );
+
+          incomingStatuses.forEach((newStatus) => {
+            const participationStatus = documentation.participations.find(
+              (part) => part.id === newStatus.id,
+            );
+            participationStatus.absenceReason = newStatus.absenceReason;
+            participationStatus.isOptimistic = newStatus.isOptimistic;
+          });
+
+          return storedDocumentations;
+        },
+      );
+      this.$once("save", this.handleSave);
+    },
+    handleSave() {
+      this.cancel();
+      this.$toastSuccess("alsijil.coursebook.absences.success");
+    },
+  },
+};
+</script>
diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/absences/AbsenceCreationForm.vue b/aleksis/apps/alsijil/frontend/components/coursebook/absences/AbsenceCreationForm.vue
new file mode 100644
index 0000000000000000000000000000000000000000..559824bdf4cb8bb214d1be3acebba38c08c836cd
--- /dev/null
+++ b/aleksis/apps/alsijil/frontend/components/coursebook/absences/AbsenceCreationForm.vue
@@ -0,0 +1,117 @@
+<template>
+  <v-form @input="$emit('valid', $event)">
+    <v-container>
+      <v-row>
+        <div aria-required="true" class="full-width">
+          <!-- FIXME Vue 3: clear-on-select -->
+          <v-autocomplete
+            :label="$t('forms.labels.persons')"
+            :items="allPersons"
+            item-text="fullName"
+            return-object
+            multiple
+            chips
+            deletable-chips
+            :rules="
+              $rules().build([
+                (value) => value.length > 0 || $t('forms.errors.required'),
+              ])
+            "
+            :value="persons"
+            :loading="$apollo.queries.allPersons.loading"
+            @input="$emit('persons', $event)"
+          />
+        </div>
+      </v-row>
+      <v-row>
+        <v-col cols="12" :sm="6" class="pl-0">
+          <div aria-required="true">
+            <date-field
+              :label="$t('forms.labels.start')"
+              :max="endDate"
+              :rules="$rules().required.build()"
+              :value="startDate"
+              @input="$emit('start-date', $event)"
+            />
+          </div>
+        </v-col>
+        <v-col cols="12" :sm="6" class="pr-0">
+          <div aria-required="true">
+            <date-field
+              :label="$t('forms.labels.end')"
+              :min="startDate"
+              :rules="$rules().required.build()"
+              :value="endDate"
+              @input="$emit('end-date', $event)"
+            />
+          </div>
+        </v-col>
+      </v-row>
+      <v-row>
+        <v-text-field
+          :label="$t('forms.labels.comment')"
+          :value="comment"
+          @input="$emit('comment', $event)"
+        />
+      </v-row>
+      <v-row>
+        <div aria-required="true">
+          <absence-reason-group-select
+            :rules="$rules().required.build()"
+            :value="absenceReason"
+            @input="$emit('absence-reason', $event)"
+          />
+        </div>
+      </v-row>
+    </v-container>
+  </v-form>
+</template>
+
+<script>
+import AbsenceReasonGroupSelect from "aleksis.apps.kolego/components/AbsenceReasonGroupSelect.vue";
+import DateField from "aleksis.core/components/generic/forms/DateField.vue";
+import { persons } from "./absenceCreation.graphql";
+import formRulesMixin from "aleksis.core/mixins/formRulesMixin.js";
+
+export default {
+  name: "AbsenceCreationForm",
+  components: {
+    AbsenceReasonGroupSelect,
+    DateField,
+  },
+  mixins: [formRulesMixin],
+  emits: [
+    "valid",
+    "persons",
+    "start-date",
+    "end-date",
+    "comment",
+    "absence-reason",
+  ],
+  apollo: {
+    allPersons: persons,
+  },
+  props: {
+    persons: {
+      type: Array,
+      required: true,
+    },
+    startDate: {
+      type: String,
+      required: true,
+    },
+    endDate: {
+      type: String,
+      required: true,
+    },
+    comment: {
+      type: String,
+      required: true,
+    },
+    absenceReason: {
+      type: String,
+      required: true,
+    },
+  },
+};
+</script>
diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/absences/AbsenceCreationSummary.vue b/aleksis/apps/alsijil/frontend/components/coursebook/absences/AbsenceCreationSummary.vue
new file mode 100644
index 0000000000000000000000000000000000000000..17a7795e697caeed9346c3aa13fe6b10e2f54c28
--- /dev/null
+++ b/aleksis/apps/alsijil/frontend/components/coursebook/absences/AbsenceCreationSummary.vue
@@ -0,0 +1,123 @@
+<template>
+  <div>
+    <message-box dense type="warning" class="mt-5">
+      {{ $t("alsijil.coursebook.absences.warning") }}
+    </message-box>
+    <!-- MAYBE introduce a minimal variant of CRUDIterator -->
+    <!--       with most features disabled for this list usecase -->
+    <c-r-u-d-iterator
+      i18n-key=""
+      :gql-query="gqlQuery"
+      :gql-additional-query-args="gqlArgs"
+      :enable-search="false"
+      :enable-create="false"
+      :enable-edit="false"
+      :elevated="false"
+      disable-pagination
+      hide-default-footer
+      @loading="handleLoading"
+    >
+      <template #default="{ items }">
+        <v-expansion-panels>
+          <v-expansion-panel v-for="person in items" :key="person.id">
+            <v-expansion-panel-header>
+              <div>
+                {{ persons.find((p) => p.id === person.id).fullName }}
+              </div>
+              <v-spacer />
+              <div>
+                {{
+                  $tc(
+                    "alsijil.coursebook.absences.lessons",
+                    person.lessons.length,
+                    { count: person.lessons.length },
+                  )
+                }}
+              </div>
+            </v-expansion-panel-header>
+            <v-expansion-panel-content>
+              <v-list-item
+                v-for="lesson in person.lessons"
+                class="px-0"
+                :key="lesson.id"
+              >
+                <v-row>
+                  <!-- TODO: We should extract this display & share it -->
+                  <v-col cols="3">
+                    <time :datetime="lesson.datetimeStart" class="text-no-wrap">
+                      {{
+                        $d(
+                          $parseISODate(lesson.datetimeStart),
+                          "shortWithWeekday",
+                        )
+                      }}&nbsp;
+                    </time>
+                  </v-col>
+                  <v-col cols="3">
+                    <time :datetime="lesson.datetimeStart" class="text-no-wrap">
+                      {{ $d($parseISODate(lesson.datetimeStart), "shortTime") }}
+                    </time>
+                    <span> - </span>
+                    <time :datetime="lesson.datetimeEnd" class="text-no-wrap">
+                      {{ $d($parseISODate(lesson.datetimeEnd), "shortTime") }}
+                    </time>
+                  </v-col>
+                  <v-col cols="3">
+                    {{ lesson.course?.name }}
+                  </v-col>
+                  <v-col cols="3">
+                    <subject-chip :subject="lesson.subject" />
+                  </v-col>
+                </v-row>
+              </v-list-item>
+            </v-expansion-panel-content>
+          </v-expansion-panel>
+        </v-expansion-panels>
+      </template>
+    </c-r-u-d-iterator>
+  </div>
+</template>
+
+<script>
+import CRUDIterator from "aleksis.core/components/generic/CRUDIterator.vue";
+import SubjectChip from "aleksis.apps.cursus/components/SubjectChip.vue";
+import { lessonsForPersons } from "./absenceCreation.graphql";
+import loadingMixin from "aleksis.core/mixins/loadingMixin.js";
+
+export default {
+  name: "AbsenceCreationSummary",
+  components: {
+    CRUDIterator,
+    SubjectChip,
+  },
+  mixins: [loadingMixin],
+  props: {
+    persons: {
+      type: Array,
+      required: true,
+    },
+    startDate: {
+      type: String,
+      required: true,
+    },
+    endDate: {
+      type: String,
+      required: true,
+    },
+  },
+  data() {
+    return {
+      gqlQuery: lessonsForPersons,
+    };
+  },
+  computed: {
+    gqlArgs() {
+      return {
+        persons: this.persons.map((person) => person.id),
+        start: this.startDate,
+        end: this.endDate,
+      };
+    },
+  },
+};
+</script>
diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/absences/absenceCreation.graphql b/aleksis/apps/alsijil/frontend/components/coursebook/absences/absenceCreation.graphql
new file mode 100644
index 0000000000000000000000000000000000000000..21ce30d0b871bcfbd9a5b40b671a8594948a2569
--- /dev/null
+++ b/aleksis/apps/alsijil/frontend/components/coursebook/absences/absenceCreation.graphql
@@ -0,0 +1,61 @@
+# Uses core persons query
+query persons {
+  allPersons: absenceCreationPersons {
+    id
+    fullName
+  }
+}
+
+query lessonsForPersons($persons: [ID]!, $start: Date!, $end: Date!) {
+  items: lessonsForPersons(persons: $persons, start: $start, end: $end) {
+    id
+    lessons {
+      id
+      datetimeStart
+      datetimeEnd
+      course {
+        id
+        name
+      }
+      subject {
+        id
+        name
+        shortName
+        colourFg
+        colourBg
+      }
+    }
+  }
+}
+
+# Use absencesInputType?
+mutation createAbsencesForPersons(
+  $persons: [ID]!
+  $start: Date!
+  $end: Date!
+  $comment: String
+  $reason: ID!
+) {
+  createAbsencesForPersons(
+    persons: $persons
+    start: $start
+    end: $end
+    comment: $comment
+    reason: $reason
+  ) {
+    ok
+    items: participationStatuses {
+      id
+      isOptimistic
+      relatedDocumentation {
+        id
+      }
+      absenceReason {
+        id
+        name
+        shortName
+        colour
+      }
+    }
+  }
+}
diff --git a/aleksis/apps/alsijil/frontend/messages/de.json b/aleksis/apps/alsijil/frontend/messages/de.json
index f1d6ecf6452596d6ab97e467cf0a06e6c6c98773..e47b2b78a0b7e9b9de790156b3182fbe6c4b3226 100644
--- a/aleksis/apps/alsijil/frontend/messages/de.json
+++ b/aleksis/apps/alsijil/frontend/messages/de.json
@@ -50,7 +50,14 @@
         }
       },
       "title_plural": "Kursbuch",
-      "present_number": "{present}/{total} anwesend"
+      "present_number": "{present}/{total} anwesend",
+      "absences": {
+        "title": "Abwesenheiten erfassen",
+        "summary": "Zusammenfassung",
+        "lessons": "Keine Stunden | 1 Stunde | {count} Stunden",
+        "success": "Die Abwesenheiten wurden erfolgreich erfasst.",
+        "warning": "Die folgenden Stunden liegen im ausgewählten Zeitraum. Bitte überprüfen Sie vor dem Bestätigen, ob Sie die Abwesenheiten für diese Stunden erfassen möchten."
+      }
     },
     "excuse_types": {
       "menu_title": "Entschuldigungsarten"
diff --git a/aleksis/apps/alsijil/frontend/messages/en.json b/aleksis/apps/alsijil/frontend/messages/en.json
index c74ae97abc9d412152123eb11bf74e5d75b70e93..3ad99b40cadee92070cf5a2b1ab287343b2a8e6f 100644
--- a/aleksis/apps/alsijil/frontend/messages/en.json
+++ b/aleksis/apps/alsijil/frontend/messages/en.json
@@ -85,7 +85,15 @@
       },
       "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}"
+      "no_results": "No search results for {search}",
+      "absences": {
+        "title": "Register absences",
+        "button": "Register absences",
+        "summary": "Summary",
+        "lessons": "No lessons | 1 lesson | {count} lessons",
+        "success": "The absences were registered successfully.",
+        "warning": "The following lessons are in the selected time period. Please check that you want to register the absences for these lessons before confirming."
+      }
     },
     "personal_notes": {
       "note": "Note",
diff --git a/aleksis/apps/alsijil/models.py b/aleksis/apps/alsijil/models.py
index 2036260dca1a134571315d6b0e3e4a23ac720d92..cca46c9cb755c4a116c3e157484b265b8f4f4ae4 100644
--- a/aleksis/apps/alsijil/models.py
+++ b/aleksis/apps/alsijil/models.py
@@ -8,6 +8,7 @@ from django.db import models
 from django.db.models import QuerySet
 from django.db.models.constraints import CheckConstraint
 from django.db.models.query_utils import Q
+from django.http import HttpRequest
 from django.urls import reverse
 from django.utils import timezone
 from django.utils.formats import date_format
@@ -529,6 +530,8 @@ class Documentation(CalendarEvent):
     @classmethod
     def get_documentations_for_events(
         cls,
+        datetime_start: datetime,
+        datetime_end: datetime,
         events: list,
         incomplete: Optional[bool] = False,
     ) -> tuple:
@@ -538,18 +541,28 @@ class Documentation(CalendarEvent):
         """
         docs = []
         dummies = []
+
+        # Prefetch existing documentations to speed things up
+        existing_documentations = Documentation.objects.filter(
+            datetime_start__lte=datetime_end,
+            datetime_end__gte=datetime_start,
+            amends__in=[e["REFERENCE_OBJECT"] for e in events],
+        ).prefetch_related("participations")
+
         for event in events:
             if incomplete and event["STATUS"] == "CANCELLED":
                 continue
 
             event_reference_obj = event["REFERENCE_OBJECT"]
-            existing_documentations = event_reference_obj.amended_by.filter(
-                datetime_start=event["DTSTART"].dt,
-                datetime_end=event["DTEND"].dt,
+            existing_documentations_event = filter(
+                lambda d: (
+                    d.datetime_start == event["DTSTART"].dt and d.datetime_end == event["DTEND"].dt
+                ),
+                existing_documentations,
             )
 
-            if existing_documentations.exists():
-                doc = existing_documentations.first()
+            doc = next(existing_documentations_event, None)
+            if doc:
                 if incomplete and doc.topic:
                     continue
                 docs.append(doc)
@@ -578,7 +591,7 @@ class Documentation(CalendarEvent):
                     )
                 )
 
-        return (docs, dummies)
+        return docs, dummies
 
     @classmethod
     def get_documentations_for_person(
@@ -594,7 +607,7 @@ class Documentation(CalendarEvent):
         """
         event_params = {
             "type": "PARTICIPANT",
-            "obj_id": person,
+            "id": person,
         }
 
         events = LessonEvent.get_single_events(
@@ -605,7 +618,7 @@ class Documentation(CalendarEvent):
             with_reference_object=True,
         )
 
-        return Documentation.get_documentations_for_events(events, incomplete)
+        return Documentation.get_documentations_for_events(start, end, events, incomplete)
 
     @classmethod
     def parse_dummy(
@@ -793,6 +806,34 @@ class ParticipationStatus(CalendarEvent):
 
     tardiness = models.PositiveSmallIntegerField(blank=True, null=True, verbose_name=_("Tardiness"))
 
+    @classmethod
+    def get_objects(
+        cls, request: HttpRequest | None = None, params: dict[str, any] | None = None
+    ) -> QuerySet:
+        qs = super().get_objects(request, params).select_related("person", "absence_reason")
+        if params:
+            if params.get("person"):
+                qs = qs.filter(person=params["person"])
+            elif params.get("persons"):
+                qs = qs.filter(person__in=params["persons"])
+            elif params.get("group"):
+                qs = qs.filter(groups_of_person__in=params.get("group"))
+        return qs
+
+    @classmethod
+    def value_title(
+        cls, reference_object: "ParticipationStatus", request: HttpRequest | None = None
+    ) -> str:
+        """Return the title of the calendar event."""
+        return f"{reference_object.person} ({reference_object.absence_reason})"
+
+    @classmethod
+    def value_description(
+        cls, reference_object: "ParticipationStatus", request: HttpRequest | None = None
+    ) -> str:
+        """Return the title of the calendar event."""
+        return ""
+
     def fill_from_kolego(self, kolego_absence: KolegoAbsence):
         """Take over data from a Kolego absence."""
         self.base_absence = kolego_absence
diff --git a/aleksis/apps/alsijil/rules.py b/aleksis/apps/alsijil/rules.py
index 99222b8f5d102c940d4ec0ef89f75ca94fbfd954..c1ffafbe856ffa64c91a17e3eaf062942debbe40 100644
--- a/aleksis/apps/alsijil/rules.py
+++ b/aleksis/apps/alsijil/rules.py
@@ -171,19 +171,16 @@ add_perm("alsijil.view_week_personalnote_rule", view_week_personal_notes_predica
 
 # Register absence
 view_register_absence_predicate = has_person & (
-    (
-        is_person_group_owner
-        & is_site_preference_set("alsijil", "register_absence_as_primary_group_owner")
-    )
-    | has_global_perm("alsijil.register_absence")
+    is_person_group_owner | has_global_perm("alsijil.register_absence")
 )
+add_perm("alsijil.view_register_absence_rule", view_register_absence_predicate)
 
 register_absence_predicate = has_person & (
-    view_register_absence_predicate
+    is_group_owner
+    | has_global_perm("alsijil.register_absence")
     | has_object_perm("core.register_absence_person")
     | has_person_group_object_perm("core.register_absence_group")
 )
-add_perm("alsijil.view_register_absence_rule", view_register_absence_predicate)
 add_perm("alsijil.register_absence_rule", register_absence_predicate)
 
 # View full register for group
diff --git a/aleksis/apps/alsijil/schema/__init__.py b/aleksis/apps/alsijil/schema/__init__.py
index 9a58c6fec23a53173cefd960951f7411cb8c80af..9b715583e32a391f0888b0d54d1cb29c8d5e4f79 100644
--- a/aleksis/apps/alsijil/schema/__init__.py
+++ b/aleksis/apps/alsijil/schema/__init__.py
@@ -11,9 +11,13 @@ from aleksis.apps.cursus.schema import CourseType
 from aleksis.core.models import Group, Person
 from aleksis.core.schema.base import FilterOrderList
 from aleksis.core.schema.group import GroupType
+from aleksis.core.schema.person import PersonType
 from aleksis.core.util.core_helpers import has_person
 
 from ..models import Documentation
+from .absences import (
+    AbsencesForPersonsCreateMutation,
+)
 from .documentation import (
     DocumentationBatchCreateOrUpdateMutation,
     DocumentationType,
@@ -52,6 +56,7 @@ class Query(graphene.ObjectType):
     groups_by_person = FilterOrderList(GroupType, person=graphene.ID())
     courses_of_person = FilterOrderList(CourseType, person=graphene.ID())
 
+    absence_creation_persons = graphene.List(PersonType)
     lessons_for_persons = graphene.List(
         LessonsForPersonType,
         persons=graphene.List(graphene.ID, required=True),
@@ -121,7 +126,12 @@ class Query(graphene.ObjectType):
         )
 
         # Lookup or create documentations and return them all.
-        docs, dummies = Documentation.get_documentations_for_events(events, incomplete)
+        docs, dummies = Documentation.get_documentations_for_events(
+            datetime.combine(date_start, datetime.min.time()),
+            datetime.combine(date_end, datetime.max.time()),
+            events,
+            incomplete,
+        )
         return docs + dummies
 
     @staticmethod
@@ -161,6 +171,12 @@ class Query(graphene.ObjectType):
             & Q(groups__in=Group.objects.for_current_school_term_or_all())
         ).distinct()
 
+    @staticmethod
+    def resolve_absence_creation_persons(root, info, **kwargs):
+        if not info.context.user.has_perm("alsijil.register_absence"):
+            return Person.objects.filter(member_of__owners=info.context.user.person)
+        return Person.objects.all()
+
     @staticmethod
     def resolve_lessons_for_persons(
         root,
@@ -179,7 +195,7 @@ class Query(graphene.ObjectType):
                 datetime.combine(end, datetime.max.time()),
             )
 
-            lessons_for_person.append(id=person, lessons=docs + dummies)
+            lessons_for_person.append(LessonsForPersonType(id=person, lessons=docs + dummies))
 
         return lessons_for_person
 
@@ -188,6 +204,7 @@ class Mutation(graphene.ObjectType):
     create_or_update_documentations = DocumentationBatchCreateOrUpdateMutation.Field()
     touch_documentation = TouchDocumentationMutation.Field()
     update_participation_statuses = ParticipationStatusBatchPatchMutation.Field()
+    create_absences_for_persons = AbsencesForPersonsCreateMutation.Field()
 
     create_extra_marks = ExtraMarkBatchCreateMutation.Field()
     update_extra_marks = ExtraMarkBatchPatchMutation.Field()
diff --git a/aleksis/apps/alsijil/schema/absences.py b/aleksis/apps/alsijil/schema/absences.py
new file mode 100644
index 0000000000000000000000000000000000000000..d05ac55214a36430808bd17ec22cc2f4c6767313
--- /dev/null
+++ b/aleksis/apps/alsijil/schema/absences.py
@@ -0,0 +1,59 @@
+from datetime import datetime
+
+from django.core.exceptions import PermissionDenied
+
+import graphene
+
+from aleksis.apps.kolego.models import Absence
+from aleksis.core.models import Person
+
+from ..models import ParticipationStatus
+from .participation_status import ParticipationStatusType
+
+
+class AbsencesForPersonsCreateMutation(graphene.Mutation):
+    class Arguments:
+        persons = graphene.List(graphene.ID, required=True)
+        start = graphene.Date(required=True)
+        end = graphene.Date(required=True)
+        comment = graphene.String(required=False)
+        reason = graphene.ID(required=True)
+
+    ok = graphene.Boolean()
+    participation_statuses = graphene.List(ParticipationStatusType)
+
+    @classmethod
+    def mutate(cls, root, info, persons, start, end, comment, reason):  # noqa
+        participation_statuses = []
+
+        persons = Person.objects.filter(pk__in=persons)
+
+        for person in persons:
+            if not info.context.user.has_perm("alsijil.register_absence_rule", person):
+                raise PermissionDenied()
+            kolego_absence, __ = Absence.objects.get_or_create(
+                date_start=start,
+                date_end=end,
+                reason_id=reason,
+                person=person,
+                defaults={"comment": comment},
+            )
+
+            events = ParticipationStatus.get_single_events(
+                datetime.combine(start, datetime.min.time()),
+                datetime.combine(end, datetime.max.time()),
+                None,
+                {"person": person},
+                with_reference_object=True,
+            )
+
+            for event in events:
+                participation_status = event["REFERENCE_OBJECT"]
+                participation_status.absence_reason_id = reason
+                participation_status.base_absence = kolego_absence
+                participation_status.save()
+                participation_statuses.append(participation_status)
+
+        return AbsencesForPersonsCreateMutation(
+            ok=True, participation_statuses=participation_statuses
+        )
diff --git a/aleksis/apps/alsijil/schema/documentation.py b/aleksis/apps/alsijil/schema/documentation.py
index 63aecfdfc0e5115250e2e4d59c34d41c8375530e..39eed04a4a688c4c8c0aa0dc333fc02e852ab2a4 100644
--- a/aleksis/apps/alsijil/schema/documentation.py
+++ b/aleksis/apps/alsijil/schema/documentation.py
@@ -34,7 +34,6 @@ class DocumentationType(PermissionsTypeMixin, DjangoFilterMixin, DjangoObjectTyp
             "date_start",
             "date_end",
             "teachers",
-            "participations",
         )
         filter_fields = {
             "id": ["exact", "lte", "gte"],
@@ -78,7 +77,7 @@ class DocumentationType(PermissionsTypeMixin, DjangoFilterMixin, DjangoObjectTyp
         # A dummy documentation will not have any participations
         if str(root.pk).startswith("DUMMY") or not hasattr(root, "participations"):
             return []
-        return root.participations.select_related("absence_reason", "base_absence").all()
+        return root.participations.all()
 
 
 class DocumentationInputType(graphene.InputObjectType):
diff --git a/aleksis/apps/alsijil/util/predicates.py b/aleksis/apps/alsijil/util/predicates.py
index eff4c8e7fd31314cebd62b8301867ebdecb3f3b3..e1d8f8a02760b846d35672ffddeeb73d161ece72 100644
--- a/aleksis/apps/alsijil/util/predicates.py
+++ b/aleksis/apps/alsijil/util/predicates.py
@@ -93,19 +93,14 @@ def is_group_owner(user: User, obj: Union[Group, Person]) -> bool:
 
 
 @predicate
-def is_person_group_owner(user: User, obj: Person) -> bool:
+def is_person_group_owner(user: User, obj) -> bool:
     """
     Predicate for group owners of any group.
 
     Checks whether the person linked to the user is
     the owner of any group of the given person.
     """
-    if obj:
-        for group in use_prefetched(obj, "member_of"):
-            if user.person in use_prefetched(group, "owners"):
-                return True
-        return False
-    return False
+    return Group.objects.filter(owners=user.person).exists()
 
 
 def use_prefetched(obj, attr):