diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/CoursebookDay.vue b/aleksis/apps/alsijil/frontend/components/coursebook/CoursebookDay.vue
index 14411e9ce9c1c2780402890a80aa907b246262ff..2f15c3fd031c62ff9fc96cf5c35042f2274d3321 100644
--- a/aleksis/apps/alsijil/frontend/components/coursebook/CoursebookDay.vue
+++ b/aleksis/apps/alsijil/frontend/components/coursebook/CoursebookDay.vue
@@ -1,13 +1,14 @@
 <template>
-  <v-list-item :style="{ scrollMarginTop: '145px' }" two-line>
+  <v-list-item :style="{ scrollMarginTop: '145px' }" two-line class="px-0">
     <v-list-item-content>
-      <v-subheader class="text-h6">{{
+      <v-subheader class="text-h6 px-1">{{
         $d(date, "dateWithWeekday")
       }}</v-subheader>
       <v-list max-width="100%" class="pt-0 mt-n1">
         <v-list-item
           v-for="doc in docs"
           :key="'documentation-' + (doc.oldId || doc.id)"
+          class="px-1"
         >
           <documentation-modal
             :documentation="doc"
diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/CoursebookFilters.vue b/aleksis/apps/alsijil/frontend/components/coursebook/CoursebookFilters.vue
index e5493b07cda406fddbb3fccc89e5d1b42080ab89..b47ebbfbb1d1cd8219686f05085eb0eeb8f453d3 100644
--- a/aleksis/apps/alsijil/frontend/components/coursebook/CoursebookFilters.vue
+++ b/aleksis/apps/alsijil/frontend/components/coursebook/CoursebookFilters.vue
@@ -45,6 +45,10 @@
 <script>
 import { coursesOfPerson, groupsByPerson } from "./coursebook.graphql";
 
+const TYPENAMES_TO_TYPES = {
+  CourseType: "course",
+  GroupType: "group",
+};
 export default {
   name: "CoursebookFilters",
   data() {
@@ -73,9 +77,9 @@ export default {
     selectable() {
       return [
         { header: this.$t("alsijil.coursebook.filter.groups") },
-        ...this.groups.map((group) => ({ type: "group", ...group })),
+        ...this.groups,
         { header: this.$t("alsijil.coursebook.filter.courses") },
-        ...this.courses.map((course) => ({ type: "course", ...course })),
+        ...this.courses,
       ];
     },
     selectLoading() {
@@ -86,14 +90,16 @@ export default {
     },
     currentObj() {
       return this.selectable.find(
-        (o) => o.type === this.value.objType && o.id === this.value.objId,
+        (o) =>
+          TYPENAMES_TO_TYPES[o.__typename] === this.value.objType &&
+          o.id === this.value.objId,
       );
     },
   },
   methods: {
     selectObject(selection) {
       this.$emit("input", {
-        objType: selection ? selection.type : null,
+        objType: selection ? TYPENAMES_TO_TYPES[selection.__typename] : null,
         objId: selection ? selection.id : null,
       });
     },
diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/CoursebookLoader.vue b/aleksis/apps/alsijil/frontend/components/coursebook/CoursebookLoader.vue
index a5136eb9f0c2535009fd78a02e7481aebacd12b4..52866931e7b2d31bbee85bc754a4a668066e8b73 100644
--- a/aleksis/apps/alsijil/frontend/components/coursebook/CoursebookLoader.vue
+++ b/aleksis/apps/alsijil/frontend/components/coursebook/CoursebookLoader.vue
@@ -1,12 +1,12 @@
 <template>
   <div>
-    <v-list-item v-for="i in numberOfDays" :key="'i-' + i">
+    <v-list-item v-for="i in numberOfDays" :key="'i-' + i" class="px-0">
       <v-list-item-content>
         <v-list-item-title>
           <v-skeleton-loader type="heading" />
         </v-list-item-title>
         <v-list max-width="100%">
-          <v-list-item v-for="j in numberOfDocs" :key="'j-' + j">
+          <v-list-item v-for="j in numberOfDocs" :key="'j-' + j" class="px-1">
             <DocumentationLoader />
           </v-list-item>
         </v-list>
diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/absences/AbsenceCreationDialog.vue b/aleksis/apps/alsijil/frontend/components/coursebook/absences/AbsenceCreationDialog.vue
index f4de6f18e8f84208baf4b1abf1e79b05550c27d2..51ac72d2873661f5247dfbb197f547dc5f73f5ff 100644
--- a/aleksis/apps/alsijil/frontend/components/coursebook/absences/AbsenceCreationDialog.vue
+++ b/aleksis/apps/alsijil/frontend/components/coursebook/absences/AbsenceCreationDialog.vue
@@ -5,20 +5,17 @@
       <!-- -> popup = true -->
     </template>
     <template #title>
-      <!-- Abwesenheit/Entschuldigung erfassen --> 
-      <!-- Abwesenheit/Entschuldigung Zusammenfassung --> 
+      <!-- Abwesenheit/Entschuldigung erfassen -->
+      <!-- Abwesenheit/Entschuldigung Zusammenfassung -->
     </template>
     <template #content>
-    <absence-form v-if="form" />
-    <absence-summary v-else />
+      <absence-form v-if="form" />
+      <absence-summary v-else />
     </template>
     <template #actions>
       <!-- secondary -->
       <!-- TODO: Return to form on cancel? form=true -->
-      <cancel-button
-        @click="popup = false"
-        disabled="loading"
-      />
+      <cancel-button @click="popup = false" disabled="loading" />
       <!-- primary -->
       <save-button
         v-if="form"
diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/absences/AbsenceCreationForm.vue b/aleksis/apps/alsijil/frontend/components/coursebook/absences/AbsenceCreationForm.vue
index 11e226bfb5ed523289a87862fa18d0e7aebbfcf9..09cf3ea5adbb59e39ed3b782bc5a114a22a123da 100644
--- a/aleksis/apps/alsijil/frontend/components/coursebook/absences/AbsenceCreationForm.vue
+++ b/aleksis/apps/alsijil/frontend/components/coursebook/absences/AbsenceCreationForm.vue
@@ -6,28 +6,22 @@
     </v-row>
     <v-row>
       <!-- Start -->
-      <v-col
-        cols="12"
-        :sm="6"
-        >
+      <v-col cols="12" :sm="6">
         <date-field
           :value="value"
           @input="$emit('input', $event)"
           :label="$t('date_select.label')"
           :disabled="loading"
-          />
+        />
       </v-col>
       <!-- End -->
-      <v-col
-        cols="12"
-        :sm="6"
-        >
+      <v-col cols="12" :sm="6">
         <date-field
           :value="value"
           @input="$emit('input', $event)"
           :label="$t('date_select.label')"
           :disabled="loading"
-          />
+        />
       </v-col>
     </v-row>
     <v-row>
diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/absences/AbsenceCreationSummary.vue b/aleksis/apps/alsijil/frontend/components/coursebook/absences/AbsenceCreationSummary.vue
index e255df5a069f3135d6dcd5030ab58a9ce2cfefc8..569bc64903ecb8740ba166f457956cc64ca09496 100644
--- a/aleksis/apps/alsijil/frontend/components/coursebook/absences/AbsenceCreationSummary.vue
+++ b/aleksis/apps/alsijil/frontend/components/coursebook/absences/AbsenceCreationSummary.vue
@@ -6,7 +6,7 @@
     :enable-create="false"
     :enable-edit="false"
     :elevated="false"
-    >
+  >
     <template #default="{ items }">
       <!-- expandable card per person -->
     </template>
diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/absences/ManageStudentsDialog.vue b/aleksis/apps/alsijil/frontend/components/coursebook/absences/ManageStudentsDialog.vue
new file mode 100644
index 0000000000000000000000000000000000000000..3af1db58846f37b5e7e7837dba08a4468294269e
--- /dev/null
+++ b/aleksis/apps/alsijil/frontend/components/coursebook/absences/ManageStudentsDialog.vue
@@ -0,0 +1,187 @@
+<script>
+import AbsenceReasonButtons from "aleksis.apps.kolego/components/AbsenceReasonButtons.vue";
+import AbsenceReasonChip from "aleksis.apps.kolego/components/AbsenceReasonChip.vue";
+import AbsenceReasonGroupSelect from "aleksis.apps.kolego/components/AbsenceReasonGroupSelect.vue";
+import CancelButton from "aleksis.core/components/generic/buttons/CancelButton.vue";
+import MobileFullscreenDialog from "aleksis.core/components/generic/dialogs/MobileFullscreenDialog.vue";
+import mutateMixin from "aleksis.core/mixins/mutateMixin.js";
+import documentationPartMixin from "../documentation/documentationPartMixin";
+import LessonInformation from "../documentation/LessonInformation.vue";
+import { updateParticipationStatuses } from "./participationStatus.graphql";
+import SlideIterator from "aleksis.core/components/generic/SlideIterator.vue";
+
+export default {
+  name: "ManageStudentsDialog",
+  extends: MobileFullscreenDialog,
+  components: {
+    AbsenceReasonChip,
+    AbsenceReasonGroupSelect,
+    AbsenceReasonButtons,
+    CancelButton,
+    LessonInformation,
+    MobileFullscreenDialog,
+    SlideIterator,
+  },
+  mixins: [documentationPartMixin, mutateMixin],
+  data() {
+    return {
+      dialog: false,
+      search: "",
+      loadSelected: false,
+      selected: [],
+      isExpanded: false,
+    };
+  },
+  props: {
+    loadingIndicator: {
+      type: Boolean,
+      default: false,
+      required: false,
+    },
+  },
+  computed: {
+    items() {
+      return this.documentation.participations;
+    },
+  },
+  methods: {
+    sendToServer(participations, field, value) {
+      if (field !== "absenceReason") return;
+
+      this.mutate(
+        updateParticipationStatuses,
+        {
+          input: participations.map((participation) => ({
+            id: participation.id,
+            absenceReason: value === "present" ? null : value,
+          })),
+        },
+        (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;
+        },
+      );
+    },
+    handleMultipleAction(absenceReasonId) {
+      this.loadSelected = true;
+      this.sendToServer(this.selected, "absenceReason", absenceReasonId);
+      this.$once("save", this.resetMultipleAction);
+    },
+    resetMultipleAction() {
+      this.loadSelected = false;
+      this.$set(this.selected, []);
+      this.$refs.iterator.selected = [];
+    },
+  },
+};
+</script>
+
+<template>
+  <mobile-fullscreen-dialog
+    scrollable
+    v-bind="$attrs"
+    v-on="$listeners"
+    v-model="dialog"
+  >
+    <template #activator="activator">
+      <slot name="activator" v-bind="activator" />
+    </template>
+
+    <template #title>
+      <lesson-information v-bind="documentationPartProps" :compact="false" />
+      <v-scroll-x-transition leave-absolute>
+        <v-text-field
+          v-show="!isExpanded"
+          type="search"
+          v-model="search"
+          clearable
+          rounded
+          hide-details
+          single-line
+          prepend-inner-icon="$search"
+          dense
+          outlined
+          :placeholder="$t('actions.search')"
+          class="pt-4 full-width"
+        />
+      </v-scroll-x-transition>
+      <v-scroll-x-transition>
+        <div v-show="selected.length > 0" class="full-width mt-4">
+          <absence-reason-buttons
+            allow-empty
+            empty-value="present"
+            @input="handleMultipleAction"
+          />
+        </div>
+      </v-scroll-x-transition>
+    </template>
+    <template #content>
+      <slide-iterator
+        ref="iterator"
+        v-model="selected"
+        :items="items"
+        :search="search"
+        :item-key-getter="
+          (item) => 'documentation-' + documentation.id + '-student-' + item.id
+        "
+        :is-expanded.sync="isExpanded"
+        :loading="loadingIndicator || loadSelected"
+        :load-only-selected="loadSelected"
+        :disabled="loading"
+      >
+        <template #listItemContent="{ item }">
+          <v-list-item-title>
+            {{ item.person.fullName }}
+          </v-list-item-title>
+          <v-list-item-subtitle v-if="item.absenceReason">
+            <absence-reason-chip small :absence-reason="item.absenceReason" />
+          </v-list-item-subtitle>
+        </template>
+
+        <template #expandedItem="{ item, close }">
+          <v-card-title>
+            <v-tooltip bottom>
+              <template #activator="{ on, attrs }">
+                <v-btn v-bind="attrs" v-on="on" icon @click="close">
+                  <v-icon>$prev</v-icon>
+                </v-btn>
+              </template>
+              <span v-t="'actions.back_to_overview'" />
+            </v-tooltip>
+            {{ item.person.fullName }}
+          </v-card-title>
+          <v-card-text>
+            <absence-reason-group-select
+              allow-empty
+              empty-value="present"
+              :loadSelectedChip="loading"
+              :value="item.absenceReason?.id || 'present'"
+              @input="sendToServer([item], 'absenceReason', $event)"
+            />
+          </v-card-text>
+        </template>
+      </slide-iterator>
+    </template>
+
+    <template #actions>
+      <cancel-button
+        @click="dialog = false"
+        i18n-key="actions.close"
+        v-show="$vuetify.breakpoint.mobile"
+      />
+    </template>
+  </mobile-fullscreen-dialog>
+</template>
+
+<style scoped></style>
diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/absences/ManageStudentsTrigger.vue b/aleksis/apps/alsijil/frontend/components/coursebook/absences/ManageStudentsTrigger.vue
new file mode 100644
index 0000000000000000000000000000000000000000..572036c67955b3365bb46eb69f6ab41ee86cf074
--- /dev/null
+++ b/aleksis/apps/alsijil/frontend/components/coursebook/absences/ManageStudentsTrigger.vue
@@ -0,0 +1,78 @@
+<script>
+import { DateTime } from "luxon";
+import ManageStudentsDialog from "./ManageStudentsDialog.vue";
+import documentationPartMixin from "../documentation/documentationPartMixin";
+import { touchDocumentation } from "./participationStatus.graphql";
+import mutateMixin from "aleksis.core/mixins/mutateMixin.js";
+
+export default {
+  name: "ManageStudentsTrigger",
+  components: { ManageStudentsDialog },
+  mixins: [documentationPartMixin, mutateMixin],
+  data() {
+    return {
+      canOpenParticipation: false,
+      timeout: null,
+    };
+  },
+  mounted() {
+    const lessonStart = DateTime.fromISO(this.documentation.datetimeStart);
+    const now = DateTime.now();
+    this.canOpenParticipation = now >= lessonStart;
+
+    if (!this.canOpenParticipation) {
+      this.timeout = setTimeout(
+        () => (this.canOpenParticipation = true),
+        lessonStart.diff(now).toObject().milliseconds,
+      );
+    }
+  },
+  beforeDestroy() {
+    if (this.timeout) {
+      clearTimeout(this.timeout);
+    }
+  },
+  methods: {
+    touchDocumentation() {
+      this.mutate(
+        touchDocumentation,
+        {
+          documentationId: this.documentation.id,
+        },
+        (storedDocumentations, incoming) => {
+          // ID may be different now
+          return storedDocumentations.map((doc) =>
+            doc.id === this.documentation.id
+              ? Object.assign(doc, incoming, { oldId: doc.id })
+              : doc,
+          );
+        },
+      );
+    },
+  },
+};
+</script>
+
+<template>
+  <manage-students-dialog
+    v-bind="documentationPartProps"
+    @update="() => null"
+    :loading-indicator="loading"
+  >
+    <template #activator="{ attrs, on }">
+      <v-chip
+        dense
+        color="primary"
+        outlined
+        :disabled="!canOpenParticipation || loading"
+        v-bind="attrs"
+        v-on="on"
+        @click="touchDocumentation"
+      >
+        <v-icon>$edit</v-icon>
+      </v-chip>
+    </template>
+  </manage-students-dialog>
+</template>
+
+<style scoped></style>
diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/absences/absenceCreation.graphql b/aleksis/apps/alsijil/frontend/components/coursebook/absences/absenceCreation.graphql
index 79178bfd835e5c6dbe726d53d6e00b11c7c8f61f..5bc9ee2d62125800ccb43fa0ec677956b9343e32 100644
--- a/aleksis/apps/alsijil/frontend/components/coursebook/absences/absenceCreation.graphql
+++ b/aleksis/apps/alsijil/frontend/components/coursebook/absences/absenceCreation.graphql
@@ -7,16 +7,8 @@ query persons {
   }
 }
 
-query lessonsForPersons(
-  $persons: [ID!]!
-  $start: Date!
-  $end: Date!
-) {
-  items: lessonsForPersons(
-    persons: $persons
-    start: $start
-    end: $end
-  ) {
+query lessonsForPersons($persons: [ID!]!, $start: Date!, $end: Date!) {
+  items: lessonsForPersons(persons: $persons, start: $start, end: $end) {
     id
     lessons {
       id
diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/absences/participationStatus.graphql b/aleksis/apps/alsijil/frontend/components/coursebook/absences/participationStatus.graphql
new file mode 100644
index 0000000000000000000000000000000000000000..81a3a5fb1eb3a99bef25eb9938cd254b9068981b
--- /dev/null
+++ b/aleksis/apps/alsijil/frontend/components/coursebook/absences/participationStatus.graphql
@@ -0,0 +1,42 @@
+mutation updateParticipationStatuses(
+  $input: [BatchPatchParticipationStatusInput]!
+) {
+  updateParticipationStatuses(input: $input) {
+    items: participationStatuses {
+      id
+      isOptimistic
+      relatedDocumentation {
+        id
+      }
+      absenceReason {
+        id
+        name
+        shortName
+        colour
+      }
+    }
+  }
+}
+
+mutation touchDocumentation($documentationId: ID!) {
+  touchDocumentation(documentationId: $documentationId) {
+    items: documentation {
+      id
+      participations {
+        id
+        person {
+          id
+          firstName
+          fullName
+        }
+        absenceReason {
+          id
+          name
+          shortName
+          colour
+        }
+        isOptimistic
+      }
+    }
+  }
+}
diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/coursebook.graphql b/aleksis/apps/alsijil/frontend/components/coursebook/coursebook.graphql
index 8444f9e35af335080026b221424fda598068c6fc..6348a24f189033fc60e97325c0c69cde5d11fbc9 100644
--- a/aleksis/apps/alsijil/frontend/components/coursebook/coursebook.graphql
+++ b/aleksis/apps/alsijil/frontend/components/coursebook/coursebook.graphql
@@ -9,10 +9,6 @@ query coursesOfPerson {
   courses: coursesOfPerson {
     id
     name
-    groups {
-      id
-      name
-    }
   }
 }
 
@@ -70,6 +66,21 @@ query documentationsForCoursebook(
       colourFg
       colourBg
     }
+    participations {
+      id
+      person {
+        id
+        firstName
+        fullName
+      }
+      absenceReason {
+        id
+        name
+        shortName
+        colour
+      }
+      isOptimistic
+    }
     topic
     homework
     groupNote
@@ -92,6 +103,21 @@ mutation createOrUpdateDocumentations($input: [DocumentationInputType]!) {
       homework
       groupNote
       oldId
+      participations {
+        id
+        person {
+          id
+          firstName
+          fullName
+        }
+        absenceReason {
+          id
+          name
+          shortName
+          colour
+        }
+        isOptimistic
+      }
     }
   }
 }
diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/documentation/LessonInformation.vue b/aleksis/apps/alsijil/frontend/components/coursebook/documentation/LessonInformation.vue
index 09a04bcb67c6ae618fa0b1546a0171af30323885..652609dccaf430d3a4ab138f80ee2f810b84b4af 100644
--- a/aleksis/apps/alsijil/frontend/components/coursebook/documentation/LessonInformation.vue
+++ b/aleksis/apps/alsijil/frontend/components/coursebook/documentation/LessonInformation.vue
@@ -61,7 +61,7 @@ import PersonChip from "aleksis.core/components/person/PersonChip.vue";
         v-for="teacher in documentation.teachers"
         :key="documentation.id + '-teacher-' + teacher.id"
         :person="teacher"
-        no-link
+        :no-link="compact"
         v-bind="compact ? dialogActivator.attrs : {}"
         v-on="compact ? dialogActivator.on : {}"
       />
@@ -69,7 +69,7 @@ import PersonChip from "aleksis.core/components/person/PersonChip.vue";
         v-for="teacher in amendedTeachers"
         :key="documentation.id + '-amendedTeacher-' + teacher.id"
         :person="teacher"
-        no-link
+        :no-link="compact"
         v-bind="compact ? dialogActivator.attrs : {}"
         v-on="compact ? dialogActivator.on : {}"
         class="text-decoration-line-through"
diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/documentation/LessonNotes.vue b/aleksis/apps/alsijil/frontend/components/coursebook/documentation/LessonNotes.vue
index f85633f2e6f3864a20db9136f86cee4b51311719..bc0da4a742917e0639a0c1983186fad29764babb 100644
--- a/aleksis/apps/alsijil/frontend/components/coursebook/documentation/LessonNotes.vue
+++ b/aleksis/apps/alsijil/frontend/components/coursebook/documentation/LessonNotes.vue
@@ -1,45 +1,71 @@
+<script setup>
+import AbsenceReasonChip from "aleksis.apps.kolego/components/AbsenceReasonChip.vue";
+</script>
+
 <template>
   <div
     class="d-flex align-center justify-space-between justify-md-end flex-wrap gap"
   >
-    <!-- eslint-disable @intlify/vue-i18n/no-raw-text -->
-    <v-chip dense color="success">
-      <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
-      >
-      entschuldigt
-    </v-chip>
-    <v-chip dense color="error">
-      <v-chip small dense class="mr-2" color="red darken-3 white--text"
-        >1</v-chip
-      >
-      unentschuldigt
+    <v-chip dense color="success" outlined v-if="total > 0">
+      {{ $t("alsijil.coursebook.present_number", { present, total }) }}
     </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
-      >
-      Hausaufgaben vergessen
-    </v-chip>
-    <v-chip dense color="primary" outlined>
-      <v-icon>$edit</v-icon>
-    </v-chip>
-    <!-- eslint-enable @intlify/vue-i18n/no-raw-text -->
+    <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>
+
+    <manage-students-trigger v-bind="documentationPartProps" />
   </div>
 </template>
 
 <script>
 import documentationPartMixin from "./documentationPartMixin";
+import ManageStudentsTrigger from "../absences/ManageStudentsTrigger.vue";
 
 export default {
   name: "LessonNotes",
+  components: { ManageStudentsTrigger },
   mixins: [documentationPartMixin],
+  computed: {
+    total() {
+      return this.documentation.participations.length;
+    },
+    present() {
+      return this.documentation.participations.filter(
+        (p) => p.absenceReason === null,
+      ).length;
+    },
+    absences() {
+      // Get all course attendants who have an absence reason
+      return Object.groupBy(
+        this.documentation.participations.filter(
+          (p) => p.absenceReason !== null,
+        ),
+        ({ absenceReason }) => absenceReason.id,
+      );
+    },
+  },
 };
 </script>
 
diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/documentation/documentationPartMixin.js b/aleksis/apps/alsijil/frontend/components/coursebook/documentation/documentationPartMixin.js
index 165f1d2fd157bb35bf2831fc7973f480b29ccd0a..88a8e852f8cc6e333303034fb5f590d174708886 100644
--- a/aleksis/apps/alsijil/frontend/components/coursebook/documentation/documentationPartMixin.js
+++ b/aleksis/apps/alsijil/frontend/components/coursebook/documentation/documentationPartMixin.js
@@ -10,6 +10,13 @@ export default {
       type: Object,
       required: true,
     },
+    /**
+     * The query used by the coursebook. Used to update the store when data changes.
+     */
+    affectedQuery: {
+      type: Object,
+      required: true,
+    },
     /**
      * Whether the documentation is currently in the compact mode (meaning coursebook row)
      */
@@ -38,6 +45,7 @@ export default {
         documentation: this.documentation,
         compact: this.compact,
         dialogActivator: this.dialogActivator,
+        affectedQuery: this.affectedQuery,
       };
     },
   },
diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/statistics/MockPerson.vue b/aleksis/apps/alsijil/frontend/components/coursebook/statistics/MockPerson.vue
index 23ff3da5ff379f3ee29afed54f3a5461d19e7a43..98a346ea0b916640a50795c5336018d7cfb9b01e 100644
--- a/aleksis/apps/alsijil/frontend/components/coursebook/statistics/MockPerson.vue
+++ b/aleksis/apps/alsijil/frontend/components/coursebook/statistics/MockPerson.vue
@@ -1,7 +1,5 @@
 <template>
-  <statistics-for-person-card
-    :person="{ id: 100 }"
-  />
+  <statistics-for-person-card :person="{ id: 100 }" />
 </template>
 
 <script>
@@ -10,7 +8,7 @@ import StatisticsForPersonCard from "./StatisticsForPersonCard.vue";
 export default {
   name: "MockPerson",
   components: {
-    StatisticsForPersonCard
+    StatisticsForPersonCard,
   },
   extends: "StatisticsForPersonCard",
 };
diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/statistics/StatisticsAbsencesCard.vue b/aleksis/apps/alsijil/frontend/components/coursebook/statistics/StatisticsAbsencesCard.vue
index 0c7fda2a44d68f6d7b7ce3a8ce095d171ced4a47..e2dd2205e6975e7e2d02c6ec5e96eea38ddb6696 100644
--- a/aleksis/apps/alsijil/frontend/components/coursebook/statistics/StatisticsAbsencesCard.vue
+++ b/aleksis/apps/alsijil/frontend/components/coursebook/statistics/StatisticsAbsencesCard.vue
@@ -1,9 +1,7 @@
 <template>
-  <v-skeleton-loader v-if="loading"
-    type="card"
-  />
+  <v-skeleton-loader v-if="loading" type="card" />
   <v-card v-else>
-    <v-card-text class="d-flex flex-column" style="gap: .5em">
+    <v-card-text class="d-flex flex-column" style="gap: 0.5em">
       <absence-reason-chip
         v-for="absenceReason in absenceReasons"
         :absenceReason="absenceReason.absenceReason"
@@ -19,7 +17,7 @@ import AbsenceReasonChip from "aleksis.apps.kolego/components/AbsenceReasonChip.
 export default {
   name: "StatisticsAbsencesCard",
   components: {
-    AbsenceReasonChip
+    AbsenceReasonChip,
   },
   props: {
     absenceReasons: {
diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/statistics/StatisticsExtraMarksCard.vue b/aleksis/apps/alsijil/frontend/components/coursebook/statistics/StatisticsExtraMarksCard.vue
index 2473b600dbb3e40c0f71482ee587eb5dcf4b4dbb..e7ae371559623adb49fb96ee2280d0f303247870 100644
--- a/aleksis/apps/alsijil/frontend/components/coursebook/statistics/StatisticsExtraMarksCard.vue
+++ b/aleksis/apps/alsijil/frontend/components/coursebook/statistics/StatisticsExtraMarksCard.vue
@@ -1,9 +1,7 @@
 <template>
-  <v-skeleton-loader v-if="loading"
-    type="card"
-  />
+  <v-skeleton-loader v-if="loading" type="card" />
   <v-card v-else>
-    <v-card-text class="d-flex flex-column" style="gap: .5em">
+    <v-card-text class="d-flex flex-column" style="gap: 0.5em">
       <counter-chip
         v-for="extraMark in extraMarks"
         :value="extraMark.extraMark.id"
diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/statistics/StatisticsForPersonCard.vue b/aleksis/apps/alsijil/frontend/components/coursebook/statistics/StatisticsForPersonCard.vue
index 800d3090752dd3c193ae70acfadc8e93d2bb1a85..86ff284806830d9ddb15c33441d618369ca31271 100644
--- a/aleksis/apps/alsijil/frontend/components/coursebook/statistics/StatisticsForPersonCard.vue
+++ b/aleksis/apps/alsijil/frontend/components/coursebook/statistics/StatisticsForPersonCard.vue
@@ -1,8 +1,6 @@
 <template>
   <v-card>
-    <v-skeleton-loader v-if="$apollo.loading"
-      type="card-heading"
-    />
+    <v-skeleton-loader v-if="$apollo.loading" type="card-heading" />
     <v-card-title v-else-if="compact">
       {{ $t("alsijil.coursebook.statistics.person_compact.title") }}
       <v-spacer />
@@ -36,7 +34,8 @@
           :loading="$apollo.loading"
         />
       </div>
-      <statistics-personal-notes-list v-if="compact"
+      <statistics-personal-notes-list
+        v-if="compact"
         class="flex-grow-1"
         :loading="$apollo.loading"
       />
@@ -85,9 +84,7 @@ export default {
     statistics: {
       query: statisticsByPerson,
       variables() {
-        const term = this.schoolTerm
-              ? { term: this.schoolTerm.id }
-              : {};
+        const term = this.schoolTerm ? { term: this.schoolTerm.id } : {};
 
         return {
           person: this.person.id,
diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/statistics/StatisticsForPersonPage.vue b/aleksis/apps/alsijil/frontend/components/coursebook/statistics/StatisticsForPersonPage.vue
index e1e8177551ae4fc53c2eb8f69c49e8817ba0aff8..b4fe3e012d2a2235312b802fdab9aca895805ccd 100644
--- a/aleksis/apps/alsijil/frontend/components/coursebook/statistics/StatisticsForPersonPage.vue
+++ b/aleksis/apps/alsijil/frontend/components/coursebook/statistics/StatisticsForPersonPage.vue
@@ -13,7 +13,7 @@
         :enable-create="false"
         :enable-edit="false"
         :elevated="false"
-        >
+      >
         <template #item="{ item }">
           <v-list-item :key="item.id">
             <v-list-item-content>
@@ -30,10 +30,7 @@
                   {{ $d(DateTime.fromISO(item.datetimeEnd), "shortTime") }}
                 </time>
                 <!-- teacher -->
-                <person-chip
-                  :person="item.teacher"
-                  no-link
-                />
+                <person-chip :person="item.teacher" no-link />
                 <!-- group -->
                 <div>
                   {{ item.groupShortName }}
@@ -54,13 +51,11 @@
                 >
                   {{ extraMark.name }}
                 </v-chip>
-            </v-list-item-title>
-            <v-list-item-subtitle>
-              item.personalNote
-            </v-list-item-subtitle>
-          </v-list-item-content>
-        </v-list-item>
-        <v-divider></v-divider>
+              </v-list-item-title>
+              <v-list-item-subtitle> item.personalNote </v-list-item-subtitle>
+            </v-list-item-content>
+          </v-list-item>
+          <v-divider></v-divider>
         </template>
       </c-r-u-d-iterator>
     </div>
@@ -69,7 +64,7 @@
       :compact="false"
       :person="{ id: personId }"
       :school-term="{ id: schoolTerm }"
-      />
+    />
   </div>
 </template>
 
diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/statistics/StatisticsPersonalNotesList.vue b/aleksis/apps/alsijil/frontend/components/coursebook/statistics/StatisticsPersonalNotesList.vue
index 5c3e70487584394b37d49331b0e3008796e182d0..5bb820d8c0a7d2b24d6ce7c7754cf51c1c1bb310 100644
--- a/aleksis/apps/alsijil/frontend/components/coursebook/statistics/StatisticsPersonalNotesList.vue
+++ b/aleksis/apps/alsijil/frontend/components/coursebook/statistics/StatisticsPersonalNotesList.vue
@@ -1,32 +1,20 @@
 <template>
-  <v-skeleton-loader v-if="loading"
-    type="card"
-  />
+  <v-skeleton-loader v-if="loading" type="card" />
   <v-card v-else>
-    <v-virtual-scroll
-        :items="personalNotes"
-        height="150"
-        item-height="75"
-      >
+    <v-virtual-scroll :items="personalNotes" height="150" item-height="75">
       <template v-slot:default="{ item }">
         <v-list-item :key="item">
           <v-list-item-content>
             <v-list-item-title class="d-flex">
               <!-- new_personal_note.documentation.course.groups.FORALL.shortName -->
-              <div>
-                5a
-              </div>
+              <div>5a</div>
               <!-- new_personal_note.documentation.subject/amends.subject -->
               <!-- TODO: In subject-chip -->
-              <div>
-                Ma
-              </div>
+              <div>Ma</div>
               <v-spacer />
               <!-- new_personal_note.documentation.datetimeStart.toDate() -->
               <div>
-                <v-list-item-subtitle>
-                  01.01.2031
-                </v-list-item-subtitle>
+                <v-list-item-subtitle> 01.01.2031 </v-list-item-subtitle>
               </div>
             </v-list-item-title>
             <v-list-item-subtitle>
@@ -37,7 +25,7 @@
         </v-list-item>
         <v-divider></v-divider>
       </template>
-    </v-virtual-scroll>      
+    </v-virtual-scroll>
   </v-card>
 </template>
 
diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/statistics/StatisticsTardinessCard.vue b/aleksis/apps/alsijil/frontend/components/coursebook/statistics/StatisticsTardinessCard.vue
index 5374b602801b598543a4ef3258b8bfb4f4d471e0..8921bcf059d0871e9682317eec8e3418893c62e7 100644
--- a/aleksis/apps/alsijil/frontend/components/coursebook/statistics/StatisticsTardinessCard.vue
+++ b/aleksis/apps/alsijil/frontend/components/coursebook/statistics/StatisticsTardinessCard.vue
@@ -1,7 +1,5 @@
 <template>
-  <v-skeleton-loader v-if="loading"
-    type="card"
-  />
+  <v-skeleton-loader v-if="loading" type="card" />
   <v-card v-else class="text-center">
     <v-card-text>
       <div class="text-h2">
diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/statistics/statistics.graphql b/aleksis/apps/alsijil/frontend/components/coursebook/statistics/statistics.graphql
index 9414ec16f7efdb4069a076d1f462a08da5d7cece..821f961c3efe23c47be9cd7583082ba67395aa86 100644
--- a/aleksis/apps/alsijil/frontend/components/coursebook/statistics/statistics.graphql
+++ b/aleksis/apps/alsijil/frontend/components/coursebook/statistics/statistics.graphql
@@ -27,26 +27,14 @@ fragment statistics on StatisticsByPersonType {
   }
 }
 
-query statisticsByPerson (
-  $person: ID!
-  $term: ID
-) {
-  statistics: statisticsByPerson(
-    person: $person
-    term: $term
-  ) {
-  ...statistics
+query statisticsByPerson($person: ID!, $term: ID) {
+  statistics: statisticsByPerson(person: $person, term: $term) {
+    ...statistics
   }
 }
 
-query documentationsByPerson (
-  $person: ID!
-  $term: ID
-) {
-  documentations: documentationsByPerson(
-    person: $person
-    term: $term
-  ) {
+query documentationsByPerson($person: ID!, $term: ID) {
+  documentations: documentationsByPerson(person: $person, term: $term) {
     id
     datetimeStart
     datetimeEnd
@@ -83,18 +71,12 @@ query documentationsByPerson (
   }
 }
 
-query statisticsByGroup (
-  $group: ID!
-  $term: ID
-) {
-  statistics: statisticsByGroup(
-    group: $group
-    term: $term
-  ) {
-  persons {
-    id
-    fullName
-    ...statistics
+query statisticsByGroup($group: ID!, $term: ID) {
+  statistics: statisticsByGroup(group: $group, term: $term) {
+    persons {
+      id
+      fullName
+      ...statistics
     }
   }
 }
diff --git a/aleksis/apps/alsijil/frontend/index.js b/aleksis/apps/alsijil/frontend/index.js
index fac7721f62b0cdd0efa2410d53e7760d7a2c4385..d1f4bcc16b5defe71695e4f99574be55aa2e5195 100644
--- a/aleksis/apps/alsijil/frontend/index.js
+++ b/aleksis/apps/alsijil/frontend/index.js
@@ -93,7 +93,8 @@ export default {
     },
     {
       path: "stats/",
-      component: () => import("./components/coursebook/statistics/MockPerson.vue"),
+      component: () =>
+        import("./components/coursebook/statistics/MockPerson.vue"),
       name: "alsijil.coursebook_stats",
       meta: {
         inMenu: true,
@@ -106,7 +107,10 @@ export default {
     },
     {
       path: "statistics/:personId/:schoolTermId/",
-      component: () => import("./components/coursebook/statistics/StatisticsForPersonPage.vue"),
+      component: () =>
+        import(
+          "./components/coursebook/statistics/StatisticsForPersonPage.vue"
+        ),
       name: "alsijil.coursebook_statistics",
       props: true,
       meta: {
diff --git a/aleksis/apps/alsijil/frontend/messages/de.json b/aleksis/apps/alsijil/frontend/messages/de.json
index 6d0fccd933e910b0f2e30c28d510da1819037137..034f1482cb55c852f8cfbc0456cdc992a33e0b67 100644
--- a/aleksis/apps/alsijil/frontend/messages/de.json
+++ b/aleksis/apps/alsijil/frontend/messages/de.json
@@ -49,6 +49,8 @@
           }
         }
       },
+      "title_plural": "Kursbuch",
+      "present_number": "{present}/{total} anwesend",
       "statistics": {
         "person_compact": {
           "title": "Kursbuch · Statistiken"
@@ -56,14 +58,26 @@
         "person_page": {
           "title": "Statistiken"
         }
-      },
-      "title_plural": "Kursbuch"
+      }
     },
     "excuse_types": {
       "menu_title": "Entschuldigungsarten"
     },
     "extra_marks": {
-      "menu_title": "Zusätzliche Markierungen"
+      "menu_title": "Zusätzliche Markierungen",
+      "create": "Markierung erstellen",
+      "name": "Markierung",
+      "short_name": "Abkürzung",
+      "colour_fg": "Schriftfarbe",
+      "colour_bg": "Hintergrundfarbe",
+      "show_in_coursebook": "In Kursbuch-Ãœbersicht zeigen",
+      "show_in_coursebook_helptext": "Wenn aktiviert tauchen diese Markierungen in den Zeilen im Kursbuch auf."
+    },
+    "personal_notes": {
+      "note": "Notiz",
+      "create_personal_note": "Weitere Notiz",
+      "confirm_delete": "Notiz wirklich löschen?",
+      "confirm_delete_explanation": "Die Notiz \"{note}\" für {name} wird entfernt."
     },
     "group_roles": {
       "menu_title_assign": "Gruppenrollen zuweisen",
@@ -88,5 +102,8 @@
     "tardiness": {
       "plural": "Verspätungen"
     }
+  },
+  "actions": {
+    "back_to_overview": "Zurück zur Übersicht"
   }
 }
diff --git a/aleksis/apps/alsijil/frontend/messages/en.json b/aleksis/apps/alsijil/frontend/messages/en.json
index 0ccd1c78d9d3f3c8c373fcd533af2062b156be15..808126e52166aa25fca3ffa2e897b3e0322f0bfc 100644
--- a/aleksis/apps/alsijil/frontend/messages/en.json
+++ b/aleksis/apps/alsijil/frontend/messages/en.json
@@ -77,6 +77,7 @@
         "courses": "Courses",
         "filter_for_obj": "Filter for group and course"
       },
+      "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}",
       "statistics": {
@@ -88,5 +89,8 @@
         }
       }
     }
+  },
+  "actions": {
+    "back_to_overview": "Back to overview"
   }
 }
diff --git a/aleksis/apps/alsijil/managers.py b/aleksis/apps/alsijil/managers.py
index 2e8b2e558e78c84ac83674840f0a3f8b2eb259c5..7d0130805275359672d1337133eacd1b0a2b62ad 100644
--- a/aleksis/apps/alsijil/managers.py
+++ b/aleksis/apps/alsijil/managers.py
@@ -11,7 +11,7 @@ from django.utils.translation import gettext as _
 from calendarweek import CalendarWeek
 
 from aleksis.apps.chronos.managers import DateRangeQuerySetMixin
-from aleksis.core.managers import AlekSISBaseManagerWithoutMigrations
+from aleksis.core.managers import AlekSISBaseManagerWithoutMigrations, PolymorphicBaseManager
 
 if TYPE_CHECKING:
     from aleksis.core.models import Group
@@ -187,3 +187,27 @@ class GroupRoleAssignmentQuerySet(DateRangeQuerySetMixin, QuerySet):
     def for_group(self, group: "Group"):
         """Filter all role assignments for a group."""
         return self.filter(Q(groups=group) | Q(groups__child_groups=group))
+
+
+class DocumentationManager(PolymorphicBaseManager):
+    """Manager adding specific methods to documentations."""
+
+    def get_queryset(self):
+        """Ensure often used related data are loaded as well."""
+        return (
+            super()
+            .get_queryset()
+            .select_related(
+                "course",
+                "subject",
+            )
+            .prefetch_related("teachers")
+        )
+
+
+class ParticipationStatusManager(PolymorphicBaseManager):
+    """Manager adding specific methods to participation statuses."""
+
+    def get_queryset(self):
+        """Ensure often used related data are loaded as well."""
+        return super().get_queryset().select_related("person", "absence_reason", "base_absence")
diff --git a/aleksis/apps/alsijil/migrations/0022_documentation_participation_touched_at.py b/aleksis/apps/alsijil/migrations/0022_documentation_participation_touched_at.py
new file mode 100644
index 0000000000000000000000000000000000000000..ef09ddca37a893128571818368982e29bd0c219f
--- /dev/null
+++ b/aleksis/apps/alsijil/migrations/0022_documentation_participation_touched_at.py
@@ -0,0 +1,18 @@
+# Generated by Django 5.0.6 on 2024-06-06 09:36
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('alsijil', '0021_remove_participationstatus_absent_and_more'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='documentation',
+            name='participation_touched_at',
+            field=models.DateTimeField(blank=True, null=True, verbose_name='Participation touched at'),
+        ),
+    ]
diff --git a/aleksis/apps/alsijil/models.py b/aleksis/apps/alsijil/models.py
index 2bf3b78c3909ab9f5ca90e82f1e3f1d090231d07..9e68ccd06773dc8b0f6ee65ccd16c81319b94a8b 100644
--- a/aleksis/apps/alsijil/models.py
+++ b/aleksis/apps/alsijil/models.py
@@ -2,17 +2,17 @@ from datetime import date, datetime
 from typing import Optional, Union
 from urllib.parse import urlparse
 
+from django.contrib.auth.models import User
 from django.core.exceptions import PermissionDenied
 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
-from django.utils.timezone import localdate, localtime
+from django.utils.timezone import localdate, localtime, now
 from django.utils.translation import gettext_lazy as _
-from django.contrib.auth.models import User
 
 from calendarweek import CalendarWeek
 from colorfield.fields import ColorField
@@ -25,12 +25,14 @@ from aleksis.apps.alsijil.data_checks import (
     PersonalNoteOnHolidaysDataCheck,
 )
 from aleksis.apps.alsijil.managers import (
+    DocumentationManager,
     GroupRoleAssignmentManager,
     GroupRoleAssignmentQuerySet,
     GroupRoleManager,
     GroupRoleQuerySet,
     LessonDocumentationManager,
     LessonDocumentationQuerySet,
+    ParticipationStatusManager,
     PersonalNoteManager,
     PersonalNoteQuerySet,
 )
@@ -43,7 +45,7 @@ from aleksis.apps.kolego.models import Absence as KolegoAbsence
 from aleksis.apps.kolego.models import AbsenceReason
 from aleksis.core.data_checks import field_validation_data_check_factory
 from aleksis.core.mixins import ExtensibleModel, GlobalPermissionModel
-from aleksis.core.models import CalendarEvent, Group, SchoolTerm
+from aleksis.core.models import CalendarEvent, Group, Person, SchoolTerm
 from aleksis.core.util.core_helpers import get_site_preferences
 from aleksis.core.util.model_helpers import ICONS
 
@@ -461,6 +463,8 @@ class Documentation(CalendarEvent):
 
     # FIXME: DataCheck
 
+    objects = DocumentationManager()
+
     course = models.ForeignKey(
         Course,
         models.PROTECT,
@@ -486,6 +490,11 @@ class Documentation(CalendarEvent):
     homework = models.CharField(verbose_name=_("Homework"), max_length=255, blank=True)
     group_note = models.CharField(verbose_name=_("Group Note"), max_length=255, blank=True)
 
+    # Used to track whether participations have been filled in
+    participation_touched_at = models.DateTimeField(
+        blank=True, null=True, verbose_name=_("Participation touched at")
+    )
+
     def get_subject(self) -> str:
         if self.subject:
             return self.subject
@@ -519,9 +528,9 @@ class Documentation(CalendarEvent):
 
     @classmethod
     def get_documentations_for_events(
-            cls,
-            events: list,
-            incomplete: Optional[bool] = False,
+        cls,
+        events: list,
+        incomplete: Optional[bool] = False,
     ) -> tuple:
         """Get all the documentations for the events.
         Create dummy documentations if none exist.
@@ -573,11 +582,11 @@ class Documentation(CalendarEvent):
 
     @classmethod
     def get_documentations_for_person(
-            cls,
-            person: int,
-            start: datetime,
-            end: datetime,
-            incomplete: Optional[bool] = False,
+        cls,
+        person: int,
+        start: datetime,
+        end: datetime,
+        incomplete: Optional[bool] = False,
     ) -> tuple:
         """Get all the documentations for the person from start to end datetime.
         Create dummy documentations if none exist.
@@ -600,65 +609,54 @@ class Documentation(CalendarEvent):
 
     @classmethod
     def parse_dummy(
-            cls,
-            _id: str,
+        cls,
+        _id: str,
     ) -> tuple:
-        """Parse dummy id string into lesson_event, datetime_start, datetime_end.
-        """
+        """Parse dummy id string into lesson_event, datetime_start, datetime_end."""
         dummy, lesson_event_id, datetime_start_iso, datetime_end_iso = _id.split(";")
         lesson_event = LessonEvent.objects.get(id=lesson_event_id)
 
         datetime_start = datetime.fromisoformat(datetime_start_iso).astimezone(
             lesson_event.timezone
         )
-        datetime_end = datetime.fromisoformat(datetime_end_iso).astimezone(
-            lesson_event.timezone
-        )
+        datetime_end = datetime.fromisoformat(datetime_end_iso).astimezone(lesson_event.timezone)
         return (lesson_event, datetime_start, datetime_end)
 
     @classmethod
     def create_from_lesson_event(
-            cls,
-            user: User,
-            lesson_event: LessonEvent,
-            datetime_start: datetime,
-            datetime_end: datetime,
+        cls,
+        user: User,
+        lesson_event: LessonEvent,
+        datetime_start: datetime,
+        datetime_end: datetime,
     ) -> "Documentation":
-        """ Create a documentation from a lesson_event with start and end datetime.
+        """Create a documentation from a lesson_event with start and end datetime.
         User is needed for permission checking.
         """
         if not user.has_perm(
-                "alsijil.add_documentation_for_lesson_event_rule", lesson_event
-            ) or not (
-                get_site_preferences()["alsijil__allow_edit_future_documentations"] == "all"
-                or (
-                    get_site_preferences()["alsijil__allow_edit_future_documentations"]
-                    == "current_day"
-                    and datetime_start.date() <= localdate()
-                )
-                or (
-                    get_site_preferences()["alsijil__allow_edit_future_documentations"]
-                    == "current_time"
-                    and datetime_start <= localtime()
-                )
-            ):
+            "alsijil.add_documentation_for_lesson_event_rule", lesson_event
+        ) or not (
+            get_site_preferences()["alsijil__allow_edit_future_documentations"] == "all"
+            or (
+                get_site_preferences()["alsijil__allow_edit_future_documentations"] == "current_day"
+                and datetime_start.date() <= localdate()
+            )
+            or (
+                get_site_preferences()["alsijil__allow_edit_future_documentations"]
+                == "current_time"
+                and datetime_start <= localtime()
+            )
+        ):
             raise PermissionDenied()
 
         if lesson_event.amends:
-            if lesson_event.course:
-                course = lesson_event.course
-            else:
-                course = lesson_event.amends.course
+            course = lesson_event.course if lesson_event.course else lesson_event.amends.course
 
-            if lesson_event.subject:
-                subject = lesson_event.subject
-            else:
-                subject = lesson_event.amends.subject
+            subject = lesson_event.subject if lesson_event.subject else lesson_event.amends.subject
 
-            if lesson_event.teachers:
-                teachers = lesson_event.teachers
-            else:
-                teachers = lesson_event.amends.teachers
+            teachers = (
+                lesson_event.teachers if lesson_event.teachers else lesson_event.amends.teachers
+            )
         else:
             course, subject, teachers = (
                 lesson_event.course,
@@ -672,41 +670,81 @@ class Documentation(CalendarEvent):
             amends=lesson_event,
             course=course,
             subject=subject,
-            topic="",
-            homework="",
-            group_note="",
         )
         obj.teachers.set(teachers.all())
         obj.save()
 
         # Create Participation Statuses
-        # Cannot use djangos bulk_create method, as then the save method of the
-        # superclass wouldn't be called
-
-        for member in lesson_event.all_members:
-            # TODO: Check for preexisting absences in kolego
-            # TODO: maybe only create if the lesson start is in the past
-            status = ParticipationStatus.objects.create(
-                person=member,
-                related_documentation=obj,
-                datetime_start=datetime_start,
-                datetime_end=datetime_end,
-                timezone=lesson_event.timezone,
-            )
-            status.groups_of_person.set(member.member_of.all())
-            status.save()
+        obj.touch()
 
         return obj
 
     @classmethod
-    def get_or_create_by_id(cls, _id: str|int, user):
+    def get_or_create_by_id(cls, _id: str | int, user):
         if _id.startswith("DUMMY"):
             return cls.create_from_lesson_event(
                 user,
                 *cls.parse_dummy(_id),
+            ), True
+
+        return cls.objects.get(id=_id), False
+
+    def touch(self):
+        """Ensure that participation statuses are created for this documentation."""
+        if (
+            self.participation_touched_at
+            or not self.amends
+            or self.value_start_datetime(self) > now()
+        ):
+            # There is no source to update from or it's too early
+            return
+
+        lesson_event: LessonEvent = self.amends
+        all_members = lesson_event.all_members
+        member_pks = [p.pk for p in all_members]
+
+        new_persons = Person.objects.filter(Q(pk__in=member_pks)).prefetch_related("member_of")
+
+        # Get absences from Kolego
+        events = KolegoAbsence.get_single_events(
+            self.value_start_datetime(self),
+            self.value_end_datetime(self),
+            None,
+            {"persons": member_pks},
+            with_reference_object=True,
+        )
+        kolego_absences_map = {a["REFERENCE_OBJECT"].person: a["REFERENCE_OBJECT"] for a in events}
+
+        new_participations = []
+        new_groups_of_person = []
+        for person in new_persons:
+            participation_status = ParticipationStatus(
+                person=person,
+                related_documentation=self,
+                datetime_start=self.datetime_start,
+                datetime_end=self.datetime_end,
+                timezone=self.timezone,
             )
 
-        return cls.objects.get(id=_id)
+            # Take over data from Kolego absence
+            if person in kolego_absences_map:
+                participation_status.fill_from_kolego(kolego_absences_map[person])
+
+            participation_status.save()
+
+            new_groups_of_person += [
+                ParticipationStatus.groups_of_person.through(
+                    group=group, participationstatus=participation_status
+                )
+                for group in person.member_of.all()
+            ]
+            new_participations.append(participation_status)
+        ParticipationStatus.groups_of_person.through.objects.bulk_create(new_groups_of_person)
+
+        self.participation_touched_at = timezone.now()
+        self.save()
+
+        return new_participations
 
 
 class ParticipationStatus(CalendarEvent):
@@ -718,6 +756,8 @@ class ParticipationStatus(CalendarEvent):
 
     # FIXME: DataChecks
 
+    objects = ParticipationStatusManager()
+
     person = models.ForeignKey(
         "core.Person", models.CASCADE, related_name="participations", verbose_name=_("Person")
     )
@@ -750,8 +790,13 @@ class ParticipationStatus(CalendarEvent):
         verbose_name=_("Base Absence"),
     )
 
+    def fill_from_kolego(self, kolego_absence: KolegoAbsence):
+        """Take over data from a Kolego absence."""
+        self.base_absence = kolego_absence
+        self.absence_reason = kolego_absence.reason
+
     def __str__(self) -> str:
-        return f"{self.related_documentation}, {self.person}"
+        return f"{self.related_documentation.id}, {self.person}"
 
     class Meta:
         verbose_name = _("Participation Status")
diff --git a/aleksis/apps/alsijil/rules.py b/aleksis/apps/alsijil/rules.py
index b7fa4d04a23df031ff479870c17cf5f1a3d53446..9045598fdf362f0bedb57424fd73bb450d80d669 100644
--- a/aleksis/apps/alsijil/rules.py
+++ b/aleksis/apps/alsijil/rules.py
@@ -12,8 +12,10 @@ from aleksis.core.util.predicates import (
 
 from .util.predicates import (
     can_edit_documentation,
+    can_edit_participation_status,
     can_view_any_documentation,
     can_view_documentation,
+    can_view_participation_status,
     has_lesson_group_object_perm,
     has_person_group_object_perm,
     has_personal_note_group_perm,
@@ -24,6 +26,7 @@ from .util.predicates import (
     is_group_owner,
     is_group_role_assignment_group_owner,
     is_in_allowed_time_range,
+    is_in_allowed_time_range_for_participation_status,
     is_lesson_event_group_owner,
     is_lesson_event_teacher,
     is_lesson_original_teacher,
@@ -414,3 +417,21 @@ edit_documentation_predicate = (
 )
 add_perm("alsijil.edit_documentation_rule", edit_documentation_predicate)
 add_perm("alsijil.delete_documentation_rule", edit_documentation_predicate)
+
+view_participation_status_for_documentation_predicate = has_person & (
+    has_global_perm("alsijil.change_participationstatus") | can_view_participation_status
+)
+add_perm(
+    "alsijil.view_participation_status_for_documentation_rule",
+    view_participation_status_for_documentation_predicate,
+)
+
+edit_participation_status_for_documentation_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_rule",
+    edit_participation_status_for_documentation_predicate,
+)
diff --git a/aleksis/apps/alsijil/schema/__init__.py b/aleksis/apps/alsijil/schema/__init__.py
index 14725b342e721e5e9ddc79eda43c774adb789f98..ef274760f7a772748bbdd0664e74cb347d76df12 100644
--- a/aleksis/apps/alsijil/schema/__init__.py
+++ b/aleksis/apps/alsijil/schema/__init__.py
@@ -5,28 +5,31 @@ from django.db.models.query_utils import Q
 
 import graphene
 
+from aleksis.apps.chronos.models import LessonEvent
 from aleksis.apps.cursus.models import Course
 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.util.core_helpers import has_person
-from aleksis.apps.chronos.models import LessonEvent
 
 from ..models import Documentation
+from .absences import (
+    AbsencesBatchCreateMutation,
+)
 from .documentation import (
     DocumentationBatchCreateOrUpdateMutation,
     DocumentationType,
-)
-from .absences import (
     LessonsForPersonType,
-    AbsencesBatchCreateMutation,
+    TouchDocumentationMutation,
 )
+from .participation_status import ParticipationStatusBatchPatchMutation
 from .statistics import (
-    StatisticsByPersonType,
     DocumentationByPersonType,
+    StatisticsByPersonType,
 )
 
+
 class Query(graphene.ObjectType):
     documentations = FilterOrderList(DocumentationType)
     documentations_by_course_id = FilterOrderList(
@@ -173,8 +176,7 @@ class Query(graphene.ObjectType):
         end,
         **kwargs,
     ):
-        """Resolve all lesson events for each person in timeframe start to end.
-        """
+        """Resolve all lesson events for each person in timeframe start to end."""
         lessons_for_person = []
         for person in persons:
             docs, dummies = Documentation.get_documentations_for_person(
@@ -183,10 +185,7 @@ class Query(graphene.ObjectType):
                 datetime.combine(end, datetime.max.time()),
             )
 
-            lessons_for_person.append(
-                id=person,
-                lessons=docs + dummies
-            )
+            lessons_for_person.append(id=person, lessons=docs + dummies)
 
         return lessons_for_person
 
@@ -208,4 +207,6 @@ class Query(graphene.ObjectType):
 
 class Mutation(graphene.ObjectType):
     create_or_update_documentations = DocumentationBatchCreateOrUpdateMutation.Field()
+    touch_documentation = TouchDocumentationMutation.Field()
+    update_participation_statuses = ParticipationStatusBatchPatchMutation.Field()
     create_absences = AbsencesBatchCreateMutation.Field()
diff --git a/aleksis/apps/alsijil/schema/absences.py b/aleksis/apps/alsijil/schema/absences.py
index 663455671de7b8f2c420bb4ab8d332ffda3c61d2..cd79f6863a688b763993e556d1651937576508a6 100644
--- a/aleksis/apps/alsijil/schema/absences.py
+++ b/aleksis/apps/alsijil/schema/absences.py
@@ -1,14 +1,11 @@
-import graphene
 from datetime import datetime
 
+import graphene
+
 from aleksis.apps.kolego.models import Absence
 
-from .documentation import DocumentationType
 from ..models import Documentation, ParticipationStatus
 
-class LessonsForPersonType(graphene.ObjectType):
-    id = graphene.ID()  # noqa
-    lessons = graphene.List(DocumentationType)
 
 class AbsencesBatchCreateMutation(graphene.Mutation):
     class Arguments:
diff --git a/aleksis/apps/alsijil/schema/documentation.py b/aleksis/apps/alsijil/schema/documentation.py
index e8443965d140c31977ffbe3770cb870d471ba907..4a0055064abd0a9cc1bb79850390da318a808b4a 100644
--- a/aleksis/apps/alsijil/schema/documentation.py
+++ b/aleksis/apps/alsijil/schema/documentation.py
@@ -1,11 +1,7 @@
-from datetime import datetime
-
 from django.core.exceptions import PermissionDenied
-from django.utils.timezone import localdate, localtime
 
 import graphene
 from graphene_django.types import DjangoObjectType
-from guardian.shortcuts import get_objects_for_user
 from reversion import create_revision, set_comment, set_user
 
 from aleksis.apps.alsijil.util.predicates import can_edit_documentation, is_in_allowed_time_range
@@ -19,6 +15,7 @@ from aleksis.core.schema.base import (
 )
 
 from ..models import Documentation
+from .participation_status import ParticipationStatusType
 
 
 class DocumentationType(PermissionsTypeMixin, DjangoFilterMixin, DjangoObjectType):
@@ -37,6 +34,7 @@ class DocumentationType(PermissionsTypeMixin, DjangoFilterMixin, DjangoObjectTyp
             "date_start",
             "date_end",
             "teachers",
+            "participations",
         )
         filter_fields = {
             "id": ["exact", "lte", "gte"],
@@ -46,6 +44,7 @@ class DocumentationType(PermissionsTypeMixin, DjangoFilterMixin, DjangoObjectTyp
     course = graphene.Field(CourseType, required=False)
     amends = graphene.Field(lambda: LessonEventType, required=False)
     subject = graphene.Field(SubjectType, required=False)
+    participations = graphene.List(ParticipationStatusType, required=False)
 
     future_notice = graphene.Boolean(required=False)
 
@@ -69,9 +68,17 @@ class DocumentationType(PermissionsTypeMixin, DjangoFilterMixin, DjangoObjectTyp
             info.context.user, root
         )
 
-    @classmethod
-    def get_queryset(cls, queryset, info):
-        return get_objects_for_user(info.context.user, "alsijil.view_documentation", queryset)
+    @staticmethod
+    def resolve_participations(root: Documentation, info, **kwargs):
+        if not info.context.user.has_perm(
+            "alsijil.view_participation_status_for_documentation", root
+        ):
+            return []
+
+        # 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()
 
 
 class DocumentationInputType(graphene.InputObjectType):
@@ -85,6 +92,11 @@ class DocumentationInputType(graphene.InputObjectType):
     group_note = graphene.String(required=False)
 
 
+class LessonsForPersonType(graphene.ObjectType):
+    id = graphene.ID()  # noqa
+    lessons = graphene.List(DocumentationType)
+
+
 class DocumentationBatchCreateOrUpdateMutation(graphene.Mutation):
     class Arguments:
         input = graphene.List(DocumentationInputType)
@@ -97,7 +109,7 @@ class DocumentationBatchCreateOrUpdateMutation(graphene.Mutation):
 
         # Sadly, we can't use the update_or_create method since create_defaults
         # is only introduced in Django 5.0
-        obj = Documentation.get_or_create_by_id(_id, info.context.user)
+        obj, __ = Documentation.get_or_create_by_id(_id, info.context.user)
 
         if not info.context.user.has_perm("alsijil.edit_documentation_rule", obj):
             raise PermissionDenied()
@@ -125,3 +137,25 @@ class DocumentationBatchCreateOrUpdateMutation(graphene.Mutation):
             objs = [cls.create_or_update(info, doc) for doc in input]
 
         return DocumentationBatchCreateOrUpdateMutation(documentations=objs)
+
+
+class TouchDocumentationMutation(graphene.Mutation):
+    class Arguments:
+        documentation_id = graphene.ID(required=True)
+
+    documentation = graphene.Field(DocumentationType)
+
+    def mutate(root, info, documentation_id):
+        documentation, created = Documentation.get_or_create_by_id(
+            documentation_id, info.context.user
+        )
+
+        if not info.context.user.has_perm(
+            "alsijil.edit_participation_status_for_documentation_rule", documentation
+        ):
+            raise PermissionDenied()
+
+        if not created:
+            documentation.touch()
+
+        return TouchDocumentationMutation(documentation=documentation)
diff --git a/aleksis/apps/alsijil/schema/participation_status.py b/aleksis/apps/alsijil/schema/participation_status.py
new file mode 100644
index 0000000000000000000000000000000000000000..246ae52a0aab7e7bf57f102ccd7e6558d4639496
--- /dev/null
+++ b/aleksis/apps/alsijil/schema/participation_status.py
@@ -0,0 +1,46 @@
+from django.core.exceptions import PermissionDenied
+
+from graphene_django import DjangoObjectType
+
+from aleksis.apps.alsijil.models import ParticipationStatus
+from aleksis.core.schema.base import (
+    BaseBatchPatchMutation,
+    DjangoFilterMixin,
+    OptimisticResponseTypeMixin,
+    PermissionsTypeMixin,
+)
+
+
+class ParticipationStatusType(
+    OptimisticResponseTypeMixin,
+    PermissionsTypeMixin,
+    DjangoFilterMixin,
+    DjangoObjectType,
+):
+    class Meta:
+        model = ParticipationStatus
+        fields = (
+            "id",
+            "person",
+            "absence_reason",
+            "related_documentation",
+            "base_absence",
+        )
+
+
+class ParticipationStatusBatchPatchMutation(BaseBatchPatchMutation):
+    class Meta:
+        model = ParticipationStatus
+        fields = ("id", "absence_reason")  # Only the reason can be updated after creation
+        return_field_name = "participationStatuses"
+
+    @classmethod
+    def check_permissions(cls, root, info, input, *args, **kwargs):  # noqa: A002
+        pass
+
+    @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
+        ):
+            raise PermissionDenied()
diff --git a/aleksis/apps/alsijil/schema/personal_note.py b/aleksis/apps/alsijil/schema/personal_note.py
index 689bb5b7f73c08c3d8961dcc9230df677e20b383..afd44904e69ac9998fc49444c9d1422fae74e68e 100644
--- a/aleksis/apps/alsijil/schema/personal_note.py
+++ b/aleksis/apps/alsijil/schema/personal_note.py
@@ -3,12 +3,12 @@ from guardian.shortcuts import get_objects_for_user
 
 from aleksis.core.schema.base import (
     DjangoFilterMixin,
-    PermissionBatchPatchMixin,
     PermissionsTypeMixin,
 )
 
 from ..models import ExtraMark
 
+
 class ExtraMarkType(PermissionsTypeMixin, DjangoFilterMixin, DjangoObjectType):
     class Meta:
         model = ExtraMark
diff --git a/aleksis/apps/alsijil/schema/statistics.py b/aleksis/apps/alsijil/schema/statistics.py
index 070379d1b893a1048f3b716b8de1514539e00eb6..4b9188a7a1e192707f783928a191b4b900b1cd2c 100644
--- a/aleksis/apps/alsijil/schema/statistics.py
+++ b/aleksis/apps/alsijil/schema/statistics.py
@@ -1,12 +1,15 @@
 import graphene
-from aleksis.core.models import Person
-from aleksis.core.schema.person import PersonType
+
 from aleksis.apps.cursus.schema import SubjectType
 from aleksis.apps.kolego.models.absence import AbsenceReason
 from aleksis.apps.kolego.schema.absence import AbsenceReasonType
+from aleksis.core.models import Person
+from aleksis.core.schema.person import PersonType
+
 from ..models import ExtraMark
 from .personal_note import ExtraMarkType
 
+
 class AbsenceReasonWithCountType(graphene.ObjectType):
     absence_reason = graphene.Field(AbsenceReasonType)
     count = graphene.Int()
diff --git a/aleksis/apps/alsijil/util/predicates.py b/aleksis/apps/alsijil/util/predicates.py
index fe7746948d807f3afede2a8fd3a11b41358965f5..9f06195e279b6e9bc9564731146d33ea7995498d 100644
--- a/aleksis/apps/alsijil/util/predicates.py
+++ b/aleksis/apps/alsijil/util/predicates.py
@@ -2,7 +2,7 @@ from typing import Any, Union
 
 from django.contrib.auth.models import User
 from django.db.models import Q
-from django.utils.timezone import localdate, localtime
+from django.utils.timezone import localdate, now
 
 from rules import predicate
 
@@ -420,11 +420,17 @@ def can_view_any_documentation(user: User):
     """Predicate which checks if the user is allowed to view any documentation."""
     allowed_lesson_events = LessonEvent.objects.related_to_person(user.person)
 
-    return Documentation.objects.filter(
+    if allowed_lesson_events.exists():
+        return True
+
+    if Documentation.objects.filter(
         Q(teachers=user.person)
         | Q(amends__in=allowed_lesson_events)
         | Q(course__teachers=user.person)
-    ).exists()
+    ).exists():
+        return True
+
+    return False
 
 
 @predicate
@@ -440,6 +446,34 @@ def can_edit_documentation(user: User, obj: Documentation):
     return False
 
 
+@predicate
+def can_view_participation_status(user: User, obj: Documentation):
+    """Predicate which checks if the user is allowed to view participation for a documentation."""
+    if obj:
+        if is_documentation_teacher(user, obj):
+            return True
+        if obj.amends:
+            return is_lesson_event_teacher(user, obj.amends) | is_lesson_event_group_owner(
+                user, obj.amends
+            )
+        if obj.course:
+            return is_course_teacher(user, obj.course)
+    return False
+
+
+@predicate
+def can_edit_participation_status(user: User, obj: Documentation):
+    """Predicate which checks if the user is allowed to edit participation for a documentation."""
+    if obj:
+        if is_documentation_teacher(user, obj):
+            return True
+        if obj.amends:
+            return is_lesson_event_teacher(user, obj.amends) | is_lesson_event_group_owner(
+                user, obj.amends
+            )
+    return False
+
+
 @predicate
 def is_in_allowed_time_range(user: User, obj: Documentation):
     """Predicate which checks if the documentation is in the allowed time range for editing."""
@@ -447,12 +481,20 @@ def is_in_allowed_time_range(user: User, obj: Documentation):
         get_site_preferences()["alsijil__allow_edit_future_documentations"] == "all"
         or (
             get_site_preferences()["alsijil__allow_edit_future_documentations"] == "current_day"
-            and obj.datetime_start.date() <= localdate()
+            and obj.value_start_datetime(obj).date() <= localdate()
         )
         or (
             get_site_preferences()["alsijil__allow_edit_future_documentations"] == "current_time"
-            and obj.datetime_start <= localtime()
+            and obj.value_start_datetime(obj) <= now()
         )
     ):
         return True
     return False
+
+
+@predicate
+def is_in_allowed_time_range_for_participation_status(user: User, obj: Documentation):
+    """Predicate which checks if the documentation is in the allowed time range for editing."""
+    if obj and obj.value_start_datetime(obj) <= now():
+        return True
+    return False
diff --git a/pyproject.toml b/pyproject.toml
index 276683bae087de62a39719ae078ecc0b223a0590..03a0772bee8aa34ffc35dc565a1762b87745abbb 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -52,7 +52,7 @@ python = "^3.10"
 aleksis-core = "^4.0.0.dev7"
 aleksis-app-chronos = "^4.0.0.dev3"
 aleksis-app-stoelindeling = { version = "^3.0.dev1", optional = true }
-aleksis-app-kolego = "^0.1.0.dev0"
+aleksis-app-kolego = "^0.1.0.dev2"
 
 [tool.poetry.extras]
 seatingplans = ["aleksis-app-stoelindeling"]