diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 0bce4160fd70426d1078ac7b238262af44566b43..9896d170efea81df55d670f15c8df22d6ee08256 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -16,6 +16,15 @@ If you're upgrading from 3.x, there is now a migration path to use.
 Therefore, please install ``AlekSIS-App-Lesrooster`` which now
 includes parts of the legacy Chronos and the migration path.
 
+`4.0.0.dev8`_ - 2024-11-15
+--------------------------
+
+Added
+~~~~~
+
+* Widgets on person and group pages with detailed coursebook statistics
+  and including all participations/personal notes.
+
 `4.0.0.dev3`_ - 2024-07-10
 --------------------------
 
@@ -387,3 +396,4 @@ Fixed
 .. _4.0.0.dev1: https://edugit.org/AlekSIS/Official/AlekSIS-App-Alsijil/-/tags/4.0.0.dev1
 .. _4.0.0.dev2: https://edugit.org/AlekSIS/Official/AlekSIS-App-Alsijil/-/tags/4.0.0.dev2
 .. _4.0.0.dev3: https://edugit.org/AlekSIS/Official/AlekSIS-App-Alsijil/-/tags/4.0.0.dev3
+.. _4.0.0.dev8: https://edugit.org/AlekSIS/Official/AlekSIS-App-Alsijil/-/tags/4.0.0.dev8
diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/Coursebook.vue b/aleksis/apps/alsijil/frontend/components/coursebook/Coursebook.vue
index ac14821941321cd6b792c1ff8bc1bf02b03c6ae0..a9080da16f1f1378a137be40da671a6b48d07342 100644
--- a/aleksis/apps/alsijil/frontend/components/coursebook/Coursebook.vue
+++ b/aleksis/apps/alsijil/frontend/components/coursebook/Coursebook.vue
@@ -69,9 +69,9 @@
         <DocumentationLoader />
       </template>
     </infinite-scrolling-date-sorted-c-r-u-d-iterator>
-    <v-scale-transition>
-      <absence-creation-dialog v-if="pageType === 'absences'" />
-    </v-scale-transition>
+    <absence-creation-dialog
+      :absence-reasons="absenceReasons"
+    />
   </div>
 </template>
 
@@ -84,11 +84,11 @@ import CoursebookLoader from "./CoursebookLoader.vue";
 import DocumentationModal from "./documentation/DocumentationModal.vue";
 import DocumentationAbsencesModal from "./absences/DocumentationAbsencesModal.vue";
 import AbsenceCreationDialog from "./absences/AbsenceCreationDialog.vue";
-import { extraMarks } from "../extra_marks/extra_marks.graphql";
+import { extraMarks } from "./queries/extraMarks.graphql";
 import DocumentationLoader from "./documentation/DocumentationLoader.vue";
 import sendToServerMixin from "./absences/sendToServerMixin";
-import { absenceReasons } from "./absences/absenceReasons.graphql";
-import { subjects } from "aleksis.apps.cursus/components/subject.graphql";
+import { absenceReasons } from "./queries/absenceReasons.graphql";
+import { subjects } from "./queries/subjects.graphql";
 
 export default {
   name: "Coursebook",
diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/absences/AbsenceCreationDialog.vue b/aleksis/apps/alsijil/frontend/components/coursebook/absences/AbsenceCreationDialog.vue
index 7a6b4c73f5e3dbe6b6dbfa86df69b96b4a8d74a2..690b07972cd932a8d5eb0fd91609e0a662d829b0 100644
--- a/aleksis/apps/alsijil/frontend/components/coursebook/absences/AbsenceCreationDialog.vue
+++ b/aleksis/apps/alsijil/frontend/components/coursebook/absences/AbsenceCreationDialog.vue
@@ -30,6 +30,7 @@
         :end-date="endDate"
         :comment="comment"
         :absence-reason="absenceReason"
+        :absence-reasons="absenceReasons"
         @valid="formValid = $event"
         @persons="persons = $event"
         @start-date="startDate = $event"
@@ -121,6 +122,12 @@ export default {
       absenceReason: "",
     };
   },
+  props: {
+    absenceReasons: {
+      type: Array,
+      required: true,
+    },
+  },
   mounted() {
     this.addPermissions(["alsijil.view_register_absence_rule"]);
     this.clearForm();
@@ -154,14 +161,17 @@ export default {
           reason: this.absenceReason,
         },
         (storedDocumentations, incomingStatuses) => {
-          const documentation = storedDocumentations.find(
-            (doc) => doc.id === this.documentation.id,
-          );
-
           incomingStatuses.forEach((newStatus) => {
+            const documentation = storedDocumentations.find(
+              (doc) => doc.id === newStatus.relatedDocumentation.id,
+            );
+            if (!documentation) {
+              return;
+            }
             const participationStatus = documentation.participations.find(
               (part) => part.id === newStatus.id,
             );
+
             participationStatus.absenceReason = newStatus.absenceReason;
             participationStatus.isOptimistic = newStatus.isOptimistic;
           });
diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/absences/AbsenceCreationForm.vue b/aleksis/apps/alsijil/frontend/components/coursebook/absences/AbsenceCreationForm.vue
index 239c83d6bbd48700773c024967636a82f3d3c4a8..74f07cc4dcc9c24ccd42c0c8345182e5e60c6a9b 100644
--- a/aleksis/apps/alsijil/frontend/components/coursebook/absences/AbsenceCreationForm.vue
+++ b/aleksis/apps/alsijil/frontend/components/coursebook/absences/AbsenceCreationForm.vue
@@ -61,6 +61,7 @@
           <absence-reason-group-select
             :rules="$rules().required.build()"
             :value="absenceReason"
+            :custom-absence-reasons="absenceReasons"
             @input="$emit('absence-reason', $event)"
           />
         </div>
@@ -115,6 +116,10 @@ export default {
       type: String,
       required: true,
     },
+    absenceReasons: {
+      type: Array,
+      required: true,
+    },
   },
   computed: {
     maxStartTime() {
diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/absences/ManageStudentsDialog.vue b/aleksis/apps/alsijil/frontend/components/coursebook/absences/ManageStudentsDialog.vue
index 2c1479f73a0de45c1c687fea455514e3ba5af93a..1fdbd333e2fc21fb44470382e515159e1bc743fe 100644
--- a/aleksis/apps/alsijil/frontend/components/coursebook/absences/ManageStudentsDialog.vue
+++ b/aleksis/apps/alsijil/frontend/components/coursebook/absences/ManageStudentsDialog.vue
@@ -129,15 +129,18 @@ export default {
           input: this.markAsAbsentDay.participationIDs,
         },
         (storedDocumentations, incomingStatuses) => {
-          const documentation = storedDocumentations.find(
-            (doc) => doc.id === this.documentation.id,
-          );
-
           incomingStatuses.forEach((newStatus) => {
+            const documentation = storedDocumentations.find(
+              (doc) => doc.id === newStatus.relatedDocumentation.id,
+            );
+            if (!documentation) {
+              return;
+            }
             const participationStatus = documentation.participations.find(
               (part) => part.id === newStatus.id,
             );
-            participationStatus.baseAbsence = newStatus.baseAbsence;
+
+            participationStatus.absenceReason = newStatus.absenceReason;
             participationStatus.isOptimistic = newStatus.isOptimistic;
           });
 
@@ -351,6 +354,7 @@ export default {
           />
           <h4>{{ $t("alsijil.extra_marks.title_plural") }}</h4>
           <extra-mark-buttons
+            :custom-extra-marks="extraMarks"
             @input="handleMultipleAction('extraMark', $event)"
           />
           <h4>{{ $t("alsijil.personal_notes.tardiness") }}</h4>
diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/absences/absenceReasons.graphql b/aleksis/apps/alsijil/frontend/components/coursebook/absences/absenceReasons.graphql
deleted file mode 100644
index a86f608ec78d1bbc50bf558b5e0650cba5b5e8a7..0000000000000000000000000000000000000000
--- a/aleksis/apps/alsijil/frontend/components/coursebook/absences/absenceReasons.graphql
+++ /dev/null
@@ -1,11 +0,0 @@
-query absenceReasons($orderBy: [String], $filters: JSONString) {
-  items: coursebookAbsenceReasons(orderBy: $orderBy, filters: $filters) {
-    id
-    shortName
-    name
-    colour
-    default
-    canEdit
-    canDelete
-  }
-}
diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/absences/participationStatus.graphql b/aleksis/apps/alsijil/frontend/components/coursebook/absences/participationStatus.graphql
index 39eb59528c4f876eb2c75c20f187e5ded6fae2bc..a20cb091a4a3352a8e182330453972e40a539800 100644
--- a/aleksis/apps/alsijil/frontend/components/coursebook/absences/participationStatus.graphql
+++ b/aleksis/apps/alsijil/frontend/components/coursebook/absences/participationStatus.graphql
@@ -69,6 +69,15 @@ mutation extendParticipationStatuses($input: [ID]!) {
   extendParticipationStatuses(input: $input) {
     items: participations {
       id
+      relatedDocumentation {
+        id
+      }
+      absenceReason {
+        id
+        name
+        shortName
+        colour
+      }
     }
     absences {
       id
diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/coursebook.graphql b/aleksis/apps/alsijil/frontend/components/coursebook/coursebook.graphql
index a49b73f149129fa86e131b804e027908c84f65db..6ae9ba9dae7e2f1de386c9b433f73ae2ae9f3a11 100644
--- a/aleksis/apps/alsijil/frontend/components/coursebook/coursebook.graphql
+++ b/aleksis/apps/alsijil/frontend/components/coursebook/coursebook.graphql
@@ -111,7 +111,6 @@ query documentationsForCoursebook(
     oldId
     canEdit
     futureNotice
-    canDelete
     futureNoticeParticipationStatus
     canEditParticipationStatus
     canViewParticipationStatus
diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/queries/absenceReasons.graphql b/aleksis/apps/alsijil/frontend/components/coursebook/queries/absenceReasons.graphql
new file mode 100644
index 0000000000000000000000000000000000000000..4de4d9cc6b3471a6e025e719730a2f9f8afe28d8
--- /dev/null
+++ b/aleksis/apps/alsijil/frontend/components/coursebook/queries/absenceReasons.graphql
@@ -0,0 +1,9 @@
+query absenceReasons {
+  items: coursebookAbsenceReasons {
+    id
+    shortName
+    name
+    colour
+    default
+  }
+}
diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/queries/extraMarks.graphql b/aleksis/apps/alsijil/frontend/components/coursebook/queries/extraMarks.graphql
new file mode 100644
index 0000000000000000000000000000000000000000..2cc007bb8d547d32ad1411bc14ce16cfc96e6dc8
--- /dev/null
+++ b/aleksis/apps/alsijil/frontend/components/coursebook/queries/extraMarks.graphql
@@ -0,0 +1,10 @@
+query extraMarks {
+  items: extraMarks {
+    id
+    shortName
+    name
+    colourFg
+    colourBg
+    showInCoursebook
+  }
+}
diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/queries/subjects.graphql b/aleksis/apps/alsijil/frontend/components/coursebook/queries/subjects.graphql
new file mode 100644
index 0000000000000000000000000000000000000000..7f6de4f2d0871c29147382b3d7e6af573c31bc80
--- /dev/null
+++ b/aleksis/apps/alsijil/frontend/components/coursebook/queries/subjects.graphql
@@ -0,0 +1,9 @@
+query subjects {
+  items: subjects {
+    id
+    name
+    shortName
+    colourFg
+    colourBg
+  }
+}
diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/statistics/StatisticsForGroupTab.vue b/aleksis/apps/alsijil/frontend/components/coursebook/statistics/StatisticsForGroupTab.vue
index f97e7b7f62b4ccee537ccb11a73bfb976c8c48d3..c8bde2e8bed22f4120dec5108f8f2f137bdfefe9 100644
--- a/aleksis/apps/alsijil/frontend/components/coursebook/statistics/StatisticsForGroupTab.vue
+++ b/aleksis/apps/alsijil/frontend/components/coursebook/statistics/StatisticsForGroupTab.vue
@@ -68,7 +68,7 @@
           name: 'alsijil.coursebook_statistics',
           params: {
             personId: item.person.id,
-            schoolTermId: schoolTerm.id,
+            mode: MODE.PARTICIPATIONS,
           },
         }"
       />
@@ -86,8 +86,9 @@ import AbsenceReasonChip from "aleksis.apps.kolego/components/AbsenceReasonChip.
 import ExtraMarkChip from "aleksis.apps.alsijil/components/extra_marks/ExtraMarkChip.vue";
 
 import { statisticsByGroup } from "./statistics.graphql";
-import { absenceReasons } from "../absences/absenceReasons.graphql";
+import { absenceReasons } from "../queries/absenceReasons.graphql";
 import { extraMarks } from "../../extra_marks/extra_marks.graphql";
+import { MODE } from "./modes";
 
 export default {
   name: "StatisticsForGroupTab",
@@ -108,6 +109,9 @@ export default {
     };
   },
   computed: {
+    MODE() {
+      return MODE;
+    },
     headers() {
       // TODO: i18n
       return [
diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/statistics/StatisticsForPersonCard.vue b/aleksis/apps/alsijil/frontend/components/coursebook/statistics/StatisticsForPersonCard.vue
index b857a1ba59b7a61263bf2cfc6f2c7ac6e8af102a..546fc8e00701798b801427355328c14d0237578b 100644
--- a/aleksis/apps/alsijil/frontend/components/coursebook/statistics/StatisticsForPersonCard.vue
+++ b/aleksis/apps/alsijil/frontend/components/coursebook/statistics/StatisticsForPersonCard.vue
@@ -15,7 +15,7 @@
           name: 'alsijil.coursebook_statistics',
           params: {
             personId: person.id,
-            schoolTermId: schoolTerm.id,
+            mode: MODE.PARTICIPATIONS,
           },
         }"
       />
@@ -24,12 +24,22 @@
       {{ $t("alsijil.coursebook.statistics.title_plural") }}
     </v-card-title>
 
-    <v-card-text>
+    <v-card-text
+      v-if="!$apollo.queries.statistics.loading && statistics == null"
+    >
+      <message-box type="error">
+        <div>{{ $t("generic_messages.error") }}</div>
+        <small>
+          {{ $t("error_code", { errorCode }) }}
+        </small>
+      </message-box>
+    </v-card-text>
+    <v-card-text v-else>
       <div class="grid">
         <statistics-absences-card
           style="grid-area: absences"
           :absence-reasons="statistics.absenceReasons"
-          :loading="$apollo.loading"
+          :loading="$apollo.queries.statistics.loading"
         />
         <statistics-tardiness-card
           style="grid-area: tardinesses"
@@ -50,17 +60,21 @@
 <script>
 import personOverviewCardMixin from "aleksis.core/mixins/personOverviewCardMixin.js";
 import BaseButton from "aleksis.core/components/generic/buttons/BaseButton.vue";
+import MessageBox from "aleksis.core/components/generic/MessageBox.vue";
 import StatisticsAbsencesCard from "./StatisticsAbsencesCard.vue";
 import StatisticsTardinessCard from "./StatisticsTardinessCard.vue";
 import StatisticsExtraMarksCard from "./StatisticsExtraMarksCard.vue";
 
 import { statisticsByPerson } from "./statistics.graphql";
+import errorCodes from "../../../errorCodes";
+import { MODE } from "./modes";
 
 export default {
   name: "StatisticsForPersonCard",
   mixins: [personOverviewCardMixin],
   components: {
     BaseButton,
+    MessageBox,
     StatisticsAbsencesCard,
     StatisticsTardinessCard,
     StatisticsExtraMarksCard,
@@ -80,25 +94,23 @@ export default {
         tardinessCount: 0,
         extraMarks: [],
       },
+      errorCode: errorCodes.statisticsEmpty,
     };
   },
   apollo: {
     statistics: {
       query: statisticsByPerson,
       variables() {
-        const term = this.schoolTerm ? { term: this.schoolTerm.id } : {};
-
         return {
           person: this.person.id,
-          ...term,
         };
       },
-      skip() {
-        return !this.schoolTerm;
-      },
     },
   },
   computed: {
+    MODE() {
+      return MODE;
+    },
     gridTemplateAreas() {
       return this.compact
         ? `"absences extra_marks" "tardinesses tardinesses"`
diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/statistics/StatisticsForPersonPage.vue b/aleksis/apps/alsijil/frontend/components/coursebook/statistics/StatisticsForPersonPage.vue
index 7df07c24db4ea90f66268135272a5fe8e634b9e4..c5a8ed51e2d89dbdd79b17e7b839141f2452a9cb 100644
--- a/aleksis/apps/alsijil/frontend/components/coursebook/statistics/StatisticsForPersonPage.vue
+++ b/aleksis/apps/alsijil/frontend/components/coursebook/statistics/StatisticsForPersonPage.vue
@@ -3,11 +3,7 @@
     :fallback-url="{ name: 'core.personById', props: { id: personId } }"
   >
     <div class="d-flex" style="gap: 4em">
-      <!-- TODO: header (close, title, print) -->
-      <!-- TODO: flex-grow-1 does a little & flex-shrink-1 does nothing -->
       <div class="flex-grow-1" style="max-width: 100%">
-        <!-- school-term-select -->
-        <school-term-field v-model="schoolTerm" :enable-create="false" />
         <!-- documentations for person list -->
         <c-r-u-d-iterator
           i18n-key="alsijil.coursebook.statistics"
@@ -19,7 +15,8 @@
         >
           <template #additionalActions>
             <v-btn-toggle
-              v-model="mode"
+              :value="mode"
+              @change="updateMode"
               mandatory
               color="secondary"
               rounded
@@ -134,17 +131,20 @@
         class="flex-shrink-1"
         :compact="false"
         :person="{ id: personId }"
-        :school-term="{ id: schoolTermId }"
       />
       <v-bottom-sheet v-model="statisticsBottomSheet" v-else>
         <statistics-for-person-card
           :compact="false"
           :person="{ id: personId }"
-          :school-term="{ id: schoolTermId }"
         />
       </v-bottom-sheet>
     </div>
     <template #actions="{ toolbar }">
+      <active-school-term-select
+        v-if="toolbar"
+        v-model="$root.activeSchoolTerm"
+        color="secondary"
+      />
       <!-- TODO: add functionality -->
       <v-btn v-if="toolbar" icon color="primary" disabled>
         <v-icon>$print</v-icon>
@@ -156,7 +156,7 @@
 
 <script>
 import AbsenceReasonChip from "aleksis.apps.kolego/components/AbsenceReasonChip.vue";
-import SchoolTermField from "aleksis.core/components/school_term/SchoolTermField.vue";
+import ActiveSchoolTermSelect from "aleksis.core/components/school_term/ActiveSchoolTermSelect.vue";
 import CRUDIterator from "aleksis.core/components/generic/CRUDIterator.vue";
 import FabButton from "aleksis.core/components/generic/buttons/FabButton.vue";
 import FullscreenDialogPage from "aleksis.core/components/generic/dialogs/FullscreenDialogPage.vue";
@@ -170,18 +170,14 @@ import {
   personName,
 } from "./statistics.graphql";
 import ExtraMarkChip from "../../extra_marks/ExtraMarkChip.vue";
-
-const MODE = {
-  PARTICIPATIONS: "PARTICIPATIONS",
-  PERSONAL_NOTES: "PERSONAL_NOTES",
-};
+import { MODE } from "./modes.js";
 
 export default {
   name: "StatisticsForPersonPage",
   components: {
+    ActiveSchoolTermSelect,
     ExtraMarkChip,
     AbsenceReasonChip,
-    SchoolTermField,
     CRUDIterator,
     FabButton,
     FullscreenDialogPage,
@@ -190,14 +186,15 @@ export default {
     StatisticsForPersonCard,
   },
   props: {
-    // personId & schoolTermId are supplied via the url
+    // personId is supplied via the url
     personId: {
       type: [Number, String],
       required: true,
     },
-    schoolTermId: {
-      type: [Number, String],
-      required: true,
+    mode: {
+      type: String,
+      required: false,
+      default: MODE.PARTICIPATIONS,
     },
   },
   apollo: {
@@ -219,7 +216,6 @@ export default {
   },
   data() {
     return {
-      mode: MODE.PARTICIPATIONS,
       statisticsBottomSheet: false,
     };
   },
@@ -227,20 +223,11 @@ export default {
     gqlQueryArgs() {
       return {
         person: this.personId,
-        term: this.schoolTermId,
       };
     },
     MODE() {
       return MODE;
     },
-    schoolTerm: {
-      get() {
-        return this.schoolTermId;
-      },
-      set(value) {
-        console.log("New SchoolTerm:", value);
-      },
-    },
   },
   methods: {
     gqlQuery() {
@@ -248,6 +235,19 @@ export default {
         ? personalNotesForPerson
         : participationsOfPerson;
     },
+    updateMode(mode = MODE.PARTICIPATIONS) {
+      if (mode === this.mode) {
+        return;
+      }
+
+      this.$router.push({
+        name: "alsijil.coursebook_statistics",
+        params: {
+          personId: this.personId,
+          mode: mode,
+        },
+      });
+    },
   },
 };
 </script>
diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/statistics/modes.js b/aleksis/apps/alsijil/frontend/components/coursebook/statistics/modes.js
new file mode 100644
index 0000000000000000000000000000000000000000..c345316a23b880f73acddf2271759de63a6e760d
--- /dev/null
+++ b/aleksis/apps/alsijil/frontend/components/coursebook/statistics/modes.js
@@ -0,0 +1,4 @@
+export const MODE = {
+  PARTICIPATIONS: "participations",
+  PERSONAL_NOTES: "personal_notes",
+};
diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/statistics/statistics.graphql b/aleksis/apps/alsijil/frontend/components/coursebook/statistics/statistics.graphql
index 68e0a08ca3804bf841fc25c57c99d4e4dc3f66ee..901bcd251829c87783bd432ad90d69a07c2cca65 100644
--- a/aleksis/apps/alsijil/frontend/components/coursebook/statistics/statistics.graphql
+++ b/aleksis/apps/alsijil/frontend/components/coursebook/statistics/statistics.graphql
@@ -30,14 +30,14 @@ fragment statistics on StatisticsByPersonType {
   }
 }
 
-query statisticsByPerson($person: ID!, $term: ID!) {
-  statistics: statisticsByPerson(person: $person, term: $term) {
+query statisticsByPerson($person: ID!) {
+  statistics: statisticsByPerson(person: $person) {
     ...statistics
   }
 }
 
-query participationsOfPerson($person: ID!, $term: ID) {
-  items: participationsOfPerson(person: $person, term: $term) {
+query participationsOfPerson($person: ID!) {
+  items: participationsOfPerson(person: $person) {
     id
     absenceReason {
       id
@@ -68,8 +68,8 @@ query participationsOfPerson($person: ID!, $term: ID) {
   }
 }
 
-query personalNotesForPerson($person: ID!, $term: ID) {
-  items: personalNotesForPerson(person: $person, term: $term) {
+query personalNotesForPerson($person: ID!) {
+  items: personalNotesForPerson(person: $person) {
     id
     note
     extraMark {
@@ -101,8 +101,8 @@ query personalNotesForPerson($person: ID!, $term: ID) {
   }
 }
 
-query statisticsByGroup($group: ID!, $term: ID) {
-  items: statisticsByGroup(group: $group, term: $term) {
+query statisticsByGroup($group: ID!) {
+  items: statisticsByGroup(group: $group) {
     #    persons {
     #      id
     #      fullName
diff --git a/aleksis/apps/alsijil/frontend/components/extra_marks/ExtraMarkButtons.vue b/aleksis/apps/alsijil/frontend/components/extra_marks/ExtraMarkButtons.vue
index 919749ac85ed97f937d5b862a6aa37d199529b30..01deef40190efb422376dc7848185e8916400b43 100644
--- a/aleksis/apps/alsijil/frontend/components/extra_marks/ExtraMarkButtons.vue
+++ b/aleksis/apps/alsijil/frontend/components/extra_marks/ExtraMarkButtons.vue
@@ -13,7 +13,7 @@ export default {
       query: extraMarks,
       update: (data) => data.items,
       skip() {
-        return this.customExtraMarks > 0;
+        return this.customExtraMarks.length > 0;
       },
     },
   },
diff --git a/aleksis/apps/alsijil/frontend/errorCodes.js b/aleksis/apps/alsijil/frontend/errorCodes.js
new file mode 100644
index 0000000000000000000000000000000000000000..27681138a971547448885dfaad4f4b14497b0ded
--- /dev/null
+++ b/aleksis/apps/alsijil/frontend/errorCodes.js
@@ -0,0 +1,17 @@
+/**
+ * Alsijil Error Codes.
+ *
+ * Schema:
+ * abb.
+ * a: A|C|D|P|S:
+ * Component inside Alsijil
+ * - Absences
+ * - Coursebook
+ * - Documentation
+ * - Personal Notes
+ * - Statistics
+ * bb: incrementing number
+ */
+export default {
+  statisticsEmpty: "ALSIJIL_S01",
+};
diff --git a/aleksis/apps/alsijil/frontend/index.js b/aleksis/apps/alsijil/frontend/index.js
index 6e16ad0a1873ad9be17ee842283c3283bd74e8b6..ac475483e76e9538c36ffb55d85f53c6531ec802 100644
--- a/aleksis/apps/alsijil/frontend/index.js
+++ b/aleksis/apps/alsijil/frontend/index.js
@@ -1,5 +1,6 @@
 import { hasPersonValidator } from "aleksis.core/routeValidators";
 import { DateTime } from "luxon";
+import { MODE } from "./components/coursebook/statistics/modes";
 
 export const collectionItems = {
   coreGroupActions: [
@@ -34,7 +35,7 @@ export const collectionItems = {
         import(
           "./components/coursebook/statistics/StatisticsForPersonCard.vue"
         ),
-      shouldDisplay: (person, currentSchoolTerm) => currentSchoolTerm != null,
+      shouldDisplay: () => true,
       colProps: {
         cols: 12,
         md: 6,
@@ -114,7 +115,7 @@ export default {
       },
     },
     {
-      path: "statistics/:personId/:schoolTermId/",
+      path: `statistics/:personId/:mode(${Object.values(MODE).join("|")})`,
       component: () =>
         import(
           "./components/coursebook/statistics/StatisticsForPersonPage.vue"
diff --git a/aleksis/apps/alsijil/managers.py b/aleksis/apps/alsijil/managers.py
index 983a29b8d73196278afc3de7b1e25f484e62f02f..b8d17a7aa95c34fd7d87314d37da943844a64771 100644
--- a/aleksis/apps/alsijil/managers.py
+++ b/aleksis/apps/alsijil/managers.py
@@ -72,18 +72,6 @@ class GroupRoleAssignmentQuerySet(QuerySet):
 class DocumentationManager(RecurrencePolymorphicManager):
     """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(RecurrencePolymorphicManager):
     """Manager adding specific methods to participation statuses."""
diff --git a/aleksis/apps/alsijil/model_extensions.py b/aleksis/apps/alsijil/model_extensions.py
index efb06db7a0b298c21fa81663a2865a976b65cf77..a3d9c14fafedc02858b025ef221d97b08bfb5b90 100644
--- a/aleksis/apps/alsijil/model_extensions.py
+++ b/aleksis/apps/alsijil/model_extensions.py
@@ -1,4 +1,4 @@
-from django.db.models import FilteredRelation, Q, QuerySet
+from django.db.models import FilteredRelation, Q, QuerySet, Value
 from django.db.models.aggregates import Count, Sum
 from django.utils.translation import gettext as _
 
@@ -34,36 +34,58 @@ Person.add_permission("register_absence_person", _("Can register an absence for
 
 
 def annotate_person_statistics(
-    persons: QuerySet[Person], participations_filter: Q, personal_notes_filter: Q
+    persons: QuerySet[Person],
+    participations_filter: Q,
+    personal_notes_filter: Q,
+    *,
+    ignore_filters: bool = False,
 ) -> QuerySet[Person]:
     """Annotate a queryset of persons with class register statistics."""
-    persons = persons.annotate(
-        filtered_participation_statuses=FilteredRelation(
-            "participations",
-            condition=(participations_filter),
-        ),
-        filtered_personal_notes=FilteredRelation(
-            "new_personal_notes",
-            condition=(personal_notes_filter),
-        ),
-    ).annotate(
-        participation_count=Count(
-            "filtered_participation_statuses",
-            filter=Q(filtered_participation_statuses__absence_reason__isnull=True),
-            distinct=True,
-        ),
-        absence_count=Count(
-            "filtered_participation_statuses",
-            filter=Q(filtered_participation_statuses__absence_reason__count_as_absent=True),
-            distinct=True,
-        ),
-        tardiness_sum=Sum("filtered_participation_statuses__tardiness", distinct=True),
-        tardiness_count=Count(
-            "filtered_participation_statuses",
-            filter=Q(filtered_participation_statuses__tardiness__gt=0),
-            distinct=True,
-        ),
-    )
+
+    if ignore_filters:
+        persons = persons.annotate(
+            absence_count=Value(0),
+            filtered_participation_statuses=FilteredRelation(
+                "participations",
+                condition=Q(pk=None),
+            ),
+            filtered_personal_notes=FilteredRelation(
+                "new_personal_notes",
+                condition=Q(pk=None),
+            ),
+            participation_count=Value(0),
+            tardiness_count=Value(0),
+            tardiness_sum=Value(0),
+        )
+    else:
+        persons = persons.annotate(
+            filtered_participation_statuses=FilteredRelation(
+                "participations",
+                condition=(participations_filter),
+            ),
+            filtered_personal_notes=FilteredRelation(
+                "new_personal_notes",
+                condition=(personal_notes_filter),
+            ),
+        ).annotate(
+            participation_count=Count(
+                "filtered_participation_statuses",
+                filter=Q(filtered_participation_statuses__absence_reason__isnull=True),
+                distinct=True,
+            ),
+            absence_count=Count(
+                "filtered_participation_statuses",
+                filter=Q(filtered_participation_statuses__absence_reason__count_as_absent=True),
+                distinct=True,
+            ),
+            tardiness_sum=Sum("filtered_participation_statuses__tardiness", distinct=True),
+            tardiness_count=Count(
+                "filtered_participation_statuses",
+                filter=Q(filtered_participation_statuses__tardiness__gt=0),
+                distinct=True,
+            ),
+        )
+
     persons = persons.order_by("last_name", "first_name")
 
     for absence_reason in AbsenceReason.objects.all():
@@ -102,6 +124,7 @@ def annotate_person_statistics_from_documentations(
         persons,
         Q(participations__related_documentation__in=docs),
         Q(new_personal_notes__documentation__in=docs),
+        ignore_filters=len(docs) == 0,
     )
 
 
diff --git a/aleksis/apps/alsijil/models.py b/aleksis/apps/alsijil/models.py
index e622c5c3d47cadfe556e6a63387919b122661153..7e10a80cb0f83f065d94198658d13c9bc3acfe30 100644
--- a/aleksis/apps/alsijil/models.py
+++ b/aleksis/apps/alsijil/models.py
@@ -142,7 +142,6 @@ class Documentation(CalendarEvent):
         events: list,
         incomplete: Optional[bool] = False,
         absences_exist: Optional[bool] = False,
-        request: Optional[HttpRequest] = None,
     ) -> tuple:
         """Get all the documentations for the events.
         Create dummy documentations if none exist.
@@ -152,11 +151,22 @@ class Documentation(CalendarEvent):
         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")
+        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",
+                "participations__person",
+                "participations__absence_reason",
+                "teachers",
+                "personal_notes",
+                "personal_notes__extra_mark",
+            )
+            .select_related("course", "subject")
+        )
 
         for event in events:
             if incomplete and event["STATUS"] == "CANCELLED":
@@ -182,6 +192,7 @@ class Documentation(CalendarEvent):
                     )
                 ):
                     continue
+                doc._amends_prefetched = event_reference_obj
                 docs.append(doc)
             elif not absences_exist:
                 if event_reference_obj.amends:
@@ -217,7 +228,6 @@ class Documentation(CalendarEvent):
         start: datetime,
         end: datetime,
         incomplete: Optional[bool] = False,
-        request: Optional[HttpRequest] = None,
     ) -> tuple:
         """Get all the documentations for the person from start to end datetime.
         Create dummy documentations if none exist.
@@ -236,7 +246,7 @@ class Documentation(CalendarEvent):
             with_reference_object=True,
         )
 
-        return Documentation.get_documentations_for_events(start, end, events, incomplete, request)
+        return Documentation.get_documentations_for_events(start, end, events, incomplete)
 
     @classmethod
     def parse_dummy(
@@ -295,15 +305,13 @@ class Documentation(CalendarEvent):
                 lesson_event.teachers,
             )
 
-        obj = cls.objects.create(
+        obj, __ = cls.objects.update_or_create(
             datetime_start=datetime_start,
             datetime_end=datetime_end,
             amends=lesson_event,
-            course=course,
-            subject=subject,
+            defaults=dict(subject=subject, course=course),
         )
         obj.teachers.set(teachers.all())
-        obj.save()
 
         # Create Participation Statuses
         obj.touch()
@@ -311,7 +319,7 @@ class Documentation(CalendarEvent):
         return obj
 
     @classmethod
-    def get_or_create_by_id(cls, _id: str | int, user):
+    def get_or_create_by_id(cls, _id: str, user):
         if _id.startswith("DUMMY"):
             return cls.create_from_lesson_event(
                 user,
@@ -467,6 +475,29 @@ class ParticipationStatus(CalendarEvent):
         """Return the title of the calendar event."""
         return ""
 
+    @classmethod
+    def set_from_kolego_by_datetimes(
+        cls, kolego_absence: KolegoAbsence, person: Person, start: datetime, end: datetime
+    ) -> list["ParticipationStatus"]:
+        participation_statuses = []
+
+        events = cls.get_single_events(
+            start,
+            end,
+            None,
+            {"person": person},
+            with_reference_object=True,
+        )
+
+        for event in events:
+            participation_status = event["REFERENCE_OBJECT"]
+            participation_status.absence_reason = kolego_absence.reason
+            participation_status.base_absence = kolego_absence
+            participation_status.save()
+            participation_statuses.append(participation_status)
+
+        return participation_statuses
+
     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/schema/__init__.py b/aleksis/apps/alsijil/schema/__init__.py
index 740aa1ff271e9e4af374f89c7b96c0e57c06a842..604cda6cd229533086533d855563a892f440d572 100644
--- a/aleksis/apps/alsijil/schema/__init__.py
+++ b/aleksis/apps/alsijil/schema/__init__.py
@@ -3,20 +3,26 @@ from datetime import datetime
 from django.db.models import BooleanField, ExpressionWrapper, Q
 
 import graphene
+import graphene_django_optimizer
 
 from aleksis.apps.chronos.models import LessonEvent
 from aleksis.apps.cursus.models import Course
 from aleksis.apps.cursus.schema import CourseType
 from aleksis.apps.kolego.models import AbsenceReason
 from aleksis.apps.kolego.schema.absence import AbsenceReasonType
-from aleksis.core.models import Group, Person, SchoolTerm
+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 get_site_preferences, has_person
+from aleksis.core.util.core_helpers import (
+    filter_active_school_term,
+    get_active_school_term,
+    get_site_preferences,
+    has_person,
+)
 
 from ..model_extensions import annotate_person_statistics_for_school_term
-from ..models import Documentation, NewPersonalNote, ParticipationStatus
+from ..models import Documentation, ExtraMark, NewPersonalNote, ParticipationStatus
 from .absences import (
     AbsencesForPersonsCreateMutation,
 )
@@ -47,7 +53,6 @@ from .statistics import StatisticsByPersonType
 
 
 class Query(graphene.ObjectType):
-    documentations = FilterOrderList(DocumentationType)
     documentations_by_course_id = FilterOrderList(
         DocumentationType, course_id=graphene.ID(required=True)
     )
@@ -80,22 +85,18 @@ class Query(graphene.ObjectType):
     statistics_by_person = graphene.Field(
         StatisticsByPersonType,
         person=graphene.ID(required=True),
-        term=graphene.ID(required=True),
     )
     participations_of_person = graphene.List(
         ParticipationStatusType,
         person=graphene.ID(required=True),
-        term=graphene.ID(required=False),
     )
     personal_notes_for_person = graphene.List(
         PersonalNoteType,
         person=graphene.ID(required=True),
-        term=graphene.ID(required=False),
     )
     statistics_by_group = graphene.List(
         StatisticsByPersonType,
         group=graphene.ID(required=True),
-        term=graphene.ID(required=False),
     )
 
     def resolve_documentations_by_course_id(root, info, course_id, **kwargs):
@@ -108,7 +109,7 @@ class Query(graphene.ObjectType):
                 )
             )
         )
-        return documentations
+        return graphene_django_optimizer.query(documentations, info)
 
     def resolve_documentations_for_coursebook(
         root,
@@ -156,6 +157,10 @@ class Query(graphene.ObjectType):
                 }
             )
 
+        school_term = get_active_school_term(info.context)
+        date_start = date_start if date_start > school_term.date_start else school_term.date_start
+        date_end = date_end if date_end < school_term.date_end else school_term.date_end
+
         events = LessonEvent.get_single_events(
             datetime.combine(date_start, datetime.min.time()),
             datetime.combine(date_end, datetime.max.time()),
@@ -171,7 +176,6 @@ class Query(graphene.ObjectType):
             events,
             incomplete,
             absences_exist,
-            info.context,
         )
         return docs + dummies
 
@@ -186,8 +190,10 @@ class Query(graphene.ObjectType):
         else:
             return []
 
+        school_term = get_active_school_term(info.context)
+
         return (
-            Group.objects.for_current_school_term_or_all()
+            Group.objects.for_school_term(school_term)
             .filter(
                 pk__in=Group.objects.filter(members=person)
                 .values_list("id", flat=True)
@@ -215,6 +221,9 @@ class Query(graphene.ObjectType):
             person = info.context.user.person
         else:
             return []
+
+        school_term = get_active_school_term(info.context)
+
         return Course.objects.filter(
             pk__in=(
                 Course.objects.filter(teachers=person)
@@ -227,20 +236,22 @@ class Query(graphene.ObjectType):
                     )
                 )
             )
-        ).filter(groups__in=Group.objects.for_current_school_term_or_all())
+        ).filter(groups__in=Group.objects.for_school_term(school_term))
 
     @staticmethod
     def resolve_absence_creation_persons(root, info, **kwargs):
         if not info.context.user.has_perm("alsijil.register_absence"):
             group_types = get_site_preferences()["alsijil__group_types_register_absence"]
+            school_term = get_active_school_term(info.context)
             if group_types:
                 return Person.objects.filter(
-                    member_of__in=Group.objects.filter(
+                    member_of__in=Group.objects.for_school_term(school_term).filter(
                         owners=info.context.user.person, group_type__in=group_types
                     )
                 )
             else:
-                return Person.objects.filter(member_of__owners=info.context.user.person)
+                qs = Person.objects.filter(member_of__owners=info.context.user.person)
+                return filter_active_school_term(info.context, qs, "member_of__school_term")
         return Person.objects.all()
 
     @staticmethod
@@ -259,13 +270,18 @@ class Query(graphene.ObjectType):
                 person,
                 start,
                 end,
-                info.context,
             )
 
             lessons_for_person.append(LessonsForPersonType(id=person, lessons=docs + dummies))
 
         return lessons_for_person
 
+    @staticmethod
+    def resolve_extra_marks(root, info, **kwargs):
+        if info.context.user.has_perm("alsijil.fetch_extramarks_rule"):
+            return ExtraMark.objects.all()
+        raise []
+
     @staticmethod
     def resolve_coursebook_absence_reasons(root, info, **kwargs):
         if not info.context.user.has_perm("kolego.fetch_absencereasons_rule"):
@@ -273,53 +289,62 @@ class Query(graphene.ObjectType):
         return AbsenceReason.objects.filter(tags__short_name="class_register")
 
     @staticmethod
-    def resolve_statistics_by_person(root, info, person, term):
+    def resolve_statistics_by_person(root, info, person):
         person = Person.objects.get(pk=person)
         if not info.context.user.has_perm("alsijil.view_person_statistics_rule", person):
             return None
-        school_term = SchoolTerm.objects.get(id=term)
-        return annotate_person_statistics_for_school_term(
-            Person.objects.filter(id=person.id), school_term
-        ).first()
+        school_term = get_active_school_term(info.context)
+        return graphene_django_optimizer.query(
+            annotate_person_statistics_for_school_term(
+                Person.objects.filter(id=person.id), school_term
+            ).first(),
+            info,
+        )
 
     @staticmethod
-    def resolve_participations_of_person(root, info, person, term=None):
+    def resolve_participations_of_person(root, info, person):
         person = Person.objects.get(pk=person)
         if not info.context.user.has_perm("alsijil.view_person_statistics_rule", person):
             return []
-        school_term = SchoolTerm.objects.get(id=term)
-        return ParticipationStatus.objects.filter(
-            person=person,
-            absence_reason__isnull=False,
-            datetime_start__date__gte=school_term.date_start,
-            datetime_end__date__lte=school_term.date_end,
-        ).order_by("-related_documentation__datetime_start")
+        school_term = get_active_school_term(info.context)
+        return graphene_django_optimizer.query(
+            ParticipationStatus.objects.filter(
+                person=person,
+                absence_reason__isnull=False,
+                datetime_start__date__gte=school_term.date_start,
+                datetime_end__date__lte=school_term.date_end,
+            ).order_by("-related_documentation__datetime_start"),
+            info,
+        )
 
     @staticmethod
-    def resolve_personal_notes_for_person(root, info, person, term=None):
+    def resolve_personal_notes_for_person(root, info, person):
         person = Person.objects.get(pk=person)
         if not info.context.user.has_perm("alsijil.view_person_statistics_rule", person):
             return []
-        school_term = SchoolTerm.objects.get(id=term)
-        return NewPersonalNote.objects.filter(
-            person=person,
-            documentation__in=Documentation.objects.filter(
-                datetime_start__date__gte=school_term.date_start,
-                datetime_end__date__lte=school_term.date_end,
-            ),
-        ).order_by("-documentation__datetime_start")
+        school_term = get_active_school_term(info.context)
+        return graphene_django_optimizer.query(
+            NewPersonalNote.objects.filter(
+                person=person,
+                documentation__in=Documentation.objects.filter(
+                    datetime_start__date__gte=school_term.date_start,
+                    datetime_end__date__lte=school_term.date_end,
+                ),
+            ).order_by("-documentation__datetime_start"),
+            info,
+        )
 
     @staticmethod
-    def resolve_statistics_by_group(root, info, group, term=None):
+    def resolve_statistics_by_group(root, info, group):
         group = Group.objects.get(pk=group)
         if not info.context.user.has_perm("alsijil.view_group_statistics_rule", group):
             return []
-        school_term = (
-            SchoolTerm.objects.get(id=term) if term is not None else SchoolTerm.get_current()
-        )
+        school_term = get_active_school_term(info.context)
 
         members = group.members.all()
-        return annotate_person_statistics_for_school_term(members, school_term, group=group)
+        return graphene_django_optimizer.query(
+            annotate_person_statistics_for_school_term(members, school_term, group=group), info
+        )
 
 
 class Mutation(graphene.ObjectType):
diff --git a/aleksis/apps/alsijil/schema/absences.py b/aleksis/apps/alsijil/schema/absences.py
index ab006e1d875e053e061d04c9c800ae0c3ef15f2e..28eca0172e7caf203214d6db27eee5c31d6c0b09 100644
--- a/aleksis/apps/alsijil/schema/absences.py
+++ b/aleksis/apps/alsijil/schema/absences.py
@@ -2,7 +2,6 @@ import datetime
 from typing import List
 
 from django.core.exceptions import PermissionDenied
-from django.db.models import Q
 
 import graphene
 
@@ -43,41 +42,18 @@ class AbsencesForPersonsCreateMutation(graphene.Mutation):
             if not info.context.user.has_perm("alsijil.register_absence_rule", person):
                 raise PermissionDenied()
 
-            # Check if there is an existing absence with overlapping datetime
-            absences = Absence.objects.filter(
-                Q(datetime_start__lte=start) | Q(date_start__lte=start.date()),
-                Q(datetime_end__gte=end) | Q(date_end__gte=end.date()),
+            kolego_absence = Absence.get_for_person_by_datetimes(
+                datetime_start=start,
+                datetime_end=end,
                 reason_id=reason,
                 person=person,
+                defaults={"comment": comment},
             )
 
-            if len(absences) > 0:
-                kolego_absence = absences.first()
-            else:
-                # Check for same times and create otherwise
-                kolego_absence, __ = Absence.objects.get_or_create(
-                    datetime_start=start,
-                    datetime_end=end,
-                    reason_id=reason,
-                    person=person,
-                    defaults={"comment": comment},
-                )
-
-            events = ParticipationStatus.get_single_events(
-                start,
-                end,
-                None,
-                {"person": person},
-                with_reference_object=True,
+            participation_statuses += ParticipationStatus.set_from_kolego_by_datetimes(
+                kolego_absence=kolego_absence, person=person, start=start, end=end
             )
 
-            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 b833de336b2a2add64ac5f0e0cf6dd7fdd8ae0c5..6b0c3e1ffb0c7a328eadb159c9407c5f8a2d74fa 100644
--- a/aleksis/apps/alsijil/schema/documentation.py
+++ b/aleksis/apps/alsijil/schema/documentation.py
@@ -1,6 +1,7 @@
 from django.core.exceptions import PermissionDenied
 
 import graphene
+from graphene_django import bypass_get_queryset
 from graphene_django.types import DjangoObjectType
 from reversion import create_revision, set_comment, set_user
 
@@ -32,7 +33,6 @@ class DocumentationType(PermissionsTypeMixin, DjangoFilterMixin, DjangoObjectTyp
         fields = (
             "id",
             "course",
-            "amends",
             "subject",
             "topic",
             "homework",
@@ -62,6 +62,14 @@ class DocumentationType(PermissionsTypeMixin, DjangoFilterMixin, DjangoObjectTyp
     old_id = graphene.ID(required=False)
 
     @staticmethod
+    @bypass_get_queryset
+    def resolve_amends(root: Documentation, info, **kwargs):
+        if hasattr(root, "_amends_prefetched"):
+            return root._amends_prefetched
+        return root.amends
+
+    @staticmethod
+    @bypass_get_queryset
     def resolve_teachers(root: Documentation, info, **kwargs):
         if not str(root.pk).startswith("DUMMY") and hasattr(root, "teachers"):
             return root.teachers
@@ -100,6 +108,7 @@ class DocumentationType(PermissionsTypeMixin, DjangoFilterMixin, DjangoObjectTyp
         )
 
     @staticmethod
+    @bypass_get_queryset
     def resolve_participations(root: Documentation, info, **kwargs):
         # A dummy documentation will not have any participations
         if str(root.pk).startswith("DUMMY") or not hasattr(root, "participations"):
@@ -112,6 +121,11 @@ class DocumentationType(PermissionsTypeMixin, DjangoFilterMixin, DjangoObjectTyp
                     p for p in root.participations.all() if p.person == info.context.user.person
                 ]
             return []
+
+        # Annotate participations with prefetched documentation data for personal notes
+        for participation in root.participations.all():
+            participation._prefetched_documentation = root
+
         return root.participations.all()
 
 
diff --git a/aleksis/apps/alsijil/schema/extra_marks.py b/aleksis/apps/alsijil/schema/extra_marks.py
index 2b1e3723d2c559e70ddd6793f5f2a89d9c1e6abe..be062b7ed3248a6836c45ee0d4d75616ed65fa54 100644
--- a/aleksis/apps/alsijil/schema/extra_marks.py
+++ b/aleksis/apps/alsijil/schema/extra_marks.py
@@ -23,12 +23,6 @@ class ExtraMarkType(
         model = ExtraMark
         fields = ("id", "short_name", "name", "colour_fg", "colour_bg", "show_in_coursebook")
 
-    @classmethod
-    def get_queryset(cls, queryset, info):
-        if info.context.user.has_perm("alsijil.fetch_extramarks_rule"):
-            return queryset
-        raise PermissionDenied()
-
 
 class ExtraMarkBatchCreateMutation(BaseBatchCreateMutation):
     class Meta:
diff --git a/aleksis/apps/alsijil/schema/participation_status.py b/aleksis/apps/alsijil/schema/participation_status.py
index 22e5820594994b2d1bb4c81ae4f5a83f615bea0b..2f6a830e2e292edd79c8cd0fce30fb14ce0f536b 100644
--- a/aleksis/apps/alsijil/schema/participation_status.py
+++ b/aleksis/apps/alsijil/schema/participation_status.py
@@ -1,6 +1,7 @@
 import datetime
 
 from django.core.exceptions import PermissionDenied
+from django.utils.formats import date_format
 from django.utils.translation import gettext_lazy as _
 
 import graphene
@@ -41,6 +42,12 @@ class ParticipationStatusType(
 
     @staticmethod
     def resolve_notes_with_extra_mark(root: ParticipationStatus, info, **kwargs):
+        if hasattr(root, "_prefetched_documentation"):
+            return [
+                p
+                for p in root._prefetched_documentation.personal_notes.all()
+                if p.person_id == root.person_id and p.extra_mark
+            ]
         return NewPersonalNote.objects.filter(
             person=root.person,
             documentation=root.related_documentation,
@@ -49,6 +56,12 @@ class ParticipationStatusType(
 
     @staticmethod
     def resolve_notes_with_note(root: ParticipationStatus, info, **kwargs):
+        if hasattr(root, "_prefetched_documentation"):
+            return [
+                p
+                for p in root._prefetched_documentation.personal_notes.all()
+                if p.person_id == root.person_id and p.note
+            ]
         return NewPersonalNote.objects.filter(
             person=root.person,
             documentation=root.related_documentation,
@@ -93,60 +106,57 @@ class ExtendParticipationStatusToAbsenceBatchMutation(graphene.Mutation):
         if participation.date_end:
             end_date = participation.date_end
         else:
-            end_date = ParticipationStatus.value_end_datetime(participation).date()
+            end_date = participation.datetime_end.date()
 
         end_datetime = datetime.datetime.combine(
             end_date, datetime.time.max, participation.timezone
         )
 
-        if participation.base_absence:
-            # Update the base absence to increase length if needed
-            absence = participation.base_absence
-
-            if absence.date_end:
-                if absence.date_end < end_date:
-                    absence.date_end = end_date
-                    absence.save()
-
-                return participation, absence
-
-            # Absence uses a datetime
-            if absence.datetime_end.astimezone(absence.timezone) < end_datetime:
-                # The end date ends after the previous absence end
-                absence.datetime_end = end_datetime
-                absence.save()
-
-            return participation, absence
+        data = dict(
+            reason=participation.absence_reason if participation.absence_reason else None,
+            person=participation.person,
+        )
 
+        if participation.date_start:
+            data["date_start"] = participation.date_start
+            data["date_end"] = end_date
+            start_datetime = datetime.datetime.combine(
+                participation.date_start, datetime.time.min, participation.timezone
+            )
         else:
-            # No base absence, simply create one if absence reason is given
-            data = dict(
-                reason_id=participation.absence_reason.id if participation.absence_reason else None,
-                person=participation.person,
+            data["datetime_start"] = participation.datetime_start
+            data["datetime_end"] = end_datetime
+            start_datetime = participation.datetime_start
+
+        defaults = dict(
+            comment=_("Extended by {full_name} on {datetime}").format(
+                full_name=info.context.user.person.full_name,
+                datetime=date_format(participation.date_start or participation.datetime_start),
             )
+        )
 
-            if participation.date_start:
-                data["date_start"] = participation.date_start
-                data["date_end"] = end_date
-            else:
-                data["datetime_start"] = ParticipationStatus.value_start_datetime(participation)
-                data["datetime_end"] = end_datetime
-
-            absence, __ = Absence.objects.get_or_create(**data)
+        absence = Absence.get_for_person_by_datetimes(**data, defaults=defaults)
 
-            participation.base_absence = absence
-            participation.save()
+        participations = ParticipationStatus.set_from_kolego_by_datetimes(
+            kolego_absence=absence,
+            person=participation.person,
+            start=start_datetime,
+            end=end_datetime,
+        )
 
-            return participation, absence
+        return participations, absence
 
     @classmethod
     def mutate(cls, root, info, input):  # noqa
         with create_revision():
             set_user(info.context.user)
             set_comment(_("Extended absence reason from coursebook."))
-            participations, absences = zip(
-                *[cls.create_absence(info, participation_id) for participation_id in input]
-            )
+            participations = []
+            absences = []
+            for participation_id in input:
+                p, a = cls.create_absence(info, participation_id)
+                participations += p
+                absences.append(a)
 
         return ExtendParticipationStatusToAbsenceBatchMutation(
             participations=participations, absences=absences
diff --git a/aleksis/apps/alsijil/schema/personal_note.py b/aleksis/apps/alsijil/schema/personal_note.py
index e37e5f0580add9255c460a7089d1e877247273eb..7a9e20edadc426c7bf6d846b002c8d04ac9d7967 100644
--- a/aleksis/apps/alsijil/schema/personal_note.py
+++ b/aleksis/apps/alsijil/schema/personal_note.py
@@ -23,7 +23,6 @@ class PersonalNoteType(
             "id",
             "note",
             "extra_mark",
-            # TODO: permissions?
             "documentation",
         )
 
diff --git a/pyproject.toml b/pyproject.toml
index 2f82bc3ed9291cd3d7a9b04d725ff6b67216942b..202de98868fb53f6d7ea481783aa616f00646aeb 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
 [tool.poetry]
 name = "AlekSIS-App-Alsijil"
-version = "4.0.0.dev8"
+version = "4.0.0.dev9"
 packages = [
     { include = "aleksis" }
 ]