diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index cf02f39e36aa6fa715f8e3c73c8069433c44f497..de300997009040e387f232f87e548db1227f1d3d 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -5,8 +5,6 @@ include:
     file: /ci/prepare/lock.yml
   - project: "AlekSIS/official/AlekSIS"
     file: /ci/test/test.yml
-  - project: "AlekSIS/official/AlekSIS"
-    file: /ci/test/lint.yml
   - project: "AlekSIS/official/AlekSIS"
     file: /ci/test/security.yml
   - project: "AlekSIS/official/AlekSIS"
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index a8339eb430c913975a3557b197afce858dcd70b8..d4635a2169b2b0b5d6377614002d39532c926c15 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -9,13 +9,22 @@ and this project adheres to `Semantic Versioning`_.
 Unreleased
 ----------
 
+`3.1.1` - 2023-07-01
+--------------------
+
 Fixed
 ~~~~~
 
 * Progress page didn't work properly.
 * About page failed to load for apps with an unknown licence.
-
-`3.1`_ - 2022-05-30
+* QUeries for persons with partial permissions failed.
+* Some pages couldn't be scrolled when a task progress popup was open.
+* Notification query failed on admin users without persons.
+* Querying for notification caused unnecessary database requests.
+* Loading bar didn't disappear on some pages after loading was finished.
+* Support newer versions of django-oauth-toolkit.
+
+`3.1`_ - 2023-05-30
 -------------------
 
 Changed
@@ -35,7 +44,7 @@ Fixed
 * Polling behavior of the whoAmI and permission queries was improved.
 * Confirmation e-mail contained a wrong link.
 
-`3.0`_ - 2022-05-11
+`3.0`_ - 2023-05-11
 -------------------
 
 Added
@@ -78,7 +87,7 @@ Fixed
 * Backend cleanup task for Celery wasn't working.
 * URLs in invitation email were broken.
 * Invitation view didn't work.
-* Invitation emails were using wrong styling. 
+* Invitation emails were using wrong styling.
 * GraphQL queries and mutations did not log exceptions.
 
 `3.0b3`_ - 2023-03-19
@@ -1173,3 +1182,4 @@ Fixed
 .. _3.0b3: https://edugit.org/AlekSIS/official/AlekSIS-Core/-/tags/3.0b3
 .. _3.0: https://edugit.org/AlekSIS/official/AlekSIS-Core/-/tags/3.0
 .. _3.1: https://edugit.org/AlekSIS/official/AlekSIS-Core/-/tags/3.1
+.. _3.1.1: https://edugit.org/AlekSIS/official/AlekSIS-Core/-/tags/3.1.1
diff --git a/aleksis/core/frontend/app/dateTimeFormats.js b/aleksis/core/frontend/app/dateTimeFormats.js
index b5d498f29c1c1a7bcfd2e59f64c695cadfcd9027..567f9a196f61052ff0481c1f975a6c0d2433964e 100644
--- a/aleksis/core/frontend/app/dateTimeFormats.js
+++ b/aleksis/core/frontend/app/dateTimeFormats.js
@@ -29,7 +29,7 @@ const dateTimeFormats = {
     shortTime: {
       hour: "numeric",
       minute: "numeric",
-    }
+    },
   },
   de: {
     short: {
@@ -60,7 +60,7 @@ const dateTimeFormats = {
     shortTime: {
       hour: "numeric",
       minute: "numeric",
-    }
+    },
   },
 };
 
diff --git a/aleksis/core/frontend/components/LegacyBaseTemplate.vue b/aleksis/core/frontend/components/LegacyBaseTemplate.vue
index 790fc10c592bac5acc1e3bbadabcae63e065804f..0105952fd08792c2cb4a472465d435a1f94a0eb3 100644
--- a/aleksis/core/frontend/components/LegacyBaseTemplate.vue
+++ b/aleksis/core/frontend/components/LegacyBaseTemplate.vue
@@ -71,7 +71,9 @@ export default {
 
       // Show loader if iframe starts to change its content, even if the $route stays the same
       this.$refs.contentIFrame.contentWindow.onpagehide = () => {
-        this.$root.contentLoading = true;
+        if (this.$root.isLegacyBaseTemplate) {
+          this.$root.contentLoading = true;
+        }
       };
 
       // Write title of iframe to SPA window
diff --git a/aleksis/core/frontend/components/app/App.vue b/aleksis/core/frontend/components/app/App.vue
index 9c3a9ced9b3e950a8c4955b96bc6afa5f79355b9..52e2801f3953024fd5071679b92ff1a1bb716f01 100644
--- a/aleksis/core/frontend/components/app/App.vue
+++ b/aleksis/core/frontend/components/app/App.vue
@@ -316,7 +316,7 @@ export default {
 </script>
 
 <style>
-div[aria-required=true] .v-input .v-label::after {
+div[aria-required="true"] .v-input .v-label::after {
   content: " *";
   color: red;
 }
diff --git a/aleksis/core/frontend/components/celery_progress/CeleryProgressBottom.vue b/aleksis/core/frontend/components/celery_progress/CeleryProgressBottom.vue
index 912fb779241ef577c6f900abc78a26008df008f6..1866c3d819ace7f6d329926f6dbe7bdc3b9b0276 100644
--- a/aleksis/core/frontend/components/celery_progress/CeleryProgressBottom.vue
+++ b/aleksis/core/frontend/components/celery_progress/CeleryProgressBottom.vue
@@ -1,5 +1,11 @@
 <template>
-  <v-bottom-sheet :value="show" persistent hide-overlay max-width="400px">
+  <v-bottom-sheet
+    :value="show"
+    persistent
+    hide-overlay
+    max-width="400px"
+    ref="sheet"
+  >
     <v-expansion-panels accordion v-model="open">
       <v-expansion-panel>
         <v-expansion-panel-header color="primary" class="white--text px-4">
@@ -33,6 +39,13 @@ export default {
   data() {
     return { open: 0 };
   },
+  mounted() {
+    // Vuetify uses the hideScroll method to disable scrolling by setting an event listener
+    // to the window. As event listeners can only be removed by referencing the listener
+    // method and because vuetify this method is called on every state change of the dialog,
+    // we simply replace the method in this component instance
+    this.$refs.sheet.hideScroll = this.$refs.sheet.showScroll;
+  },
   computed: {
     show() {
       return this.celeryProgressByUser && this.celeryProgressByUser.length > 0;
diff --git a/aleksis/core/frontend/components/generic/InlineCRUDList.vue b/aleksis/core/frontend/components/generic/InlineCRUDList.vue
index 1a31de5fc285831d43eb713725b292473253d5a3..ef1a17a91cb037586cd67f7e9abafcda1ee796b3 100644
--- a/aleksis/core/frontend/components/generic/InlineCRUDList.vue
+++ b/aleksis/core/frontend/components/generic/InlineCRUDList.vue
@@ -28,14 +28,11 @@
         :class="elevationClass"
         :items-per-page="15"
         :search="search"
-
         :sort-by.sync="sortBy"
         :sort-desc.sync="sortDesc"
         multi-sort
-
         @update:sort-by="handleSortChange"
         @update:sort-desc="handleSortChange"
-
         :show-select="generatedActions.length > 0"
         selectable-key="canDelete"
         @item-selected="handleItemSelected"
@@ -45,31 +42,28 @@
         <template #top>
           <v-toolbar flat class="height-fit child-height-fit">
             <v-row class="flex-wrap gap align-baseline pt-4">
-                <v-toolbar-title class="d-flex flex-wrap w-100 gap">
+              <v-toolbar-title class="d-flex flex-wrap w-100 gap">
                 <filter-button
-                    class="my-1 button-40"
-                    :num-filters="numFilters"
-                    v-if="filter"
-                    @click="requestFilter"
-                    @clear="clearFilters"
+                  class="my-1 button-40"
+                  :num-filters="numFilters"
+                  v-if="filter"
+                  @click="requestFilter"
+                  @clear="clearFilters"
                 />
 
-                  <filter-dialog
-                    v-model="filterDialog"
-                    :filters="filters"
-                    @filters="handleFiltersChanged"
-                  >
-                    <template #default="slotProps">
-                      <slot name="filters"
-                        v-if="filter"
-                        v-bind="slotProps"
-                      >
-                        <message-box type="error">
-                          Please implement your filter dialog!
-                        </message-box>
-                      </slot>
-                    </template>
-                  </filter-dialog>
+                <filter-dialog
+                  v-model="filterDialog"
+                  :filters="filters"
+                  @filters="handleFiltersChanged"
+                >
+                  <template #default="slotProps">
+                    <slot name="filters" v-if="filter" v-bind="slotProps">
+                      <message-box type="error">
+                        Please implement your filter dialog!
+                      </message-box>
+                    </slot>
+                  </template>
+                </filter-dialog>
 
                 <div class="my-1">
                   <v-text-field
@@ -87,33 +81,36 @@
                   ></v-text-field>
                 </div>
 
-                <div v-if="generatedActions.length > 0 && selectedItems.length > 0" class="my-1">
+                <div
+                  v-if="generatedActions.length > 0 && selectedItems.length > 0"
+                  class="my-1"
+                >
                   <v-autocomplete
-                      auto-select-first
-                      clearable
-                      :items="generatedActions"
-                      v-model="selectedAction"
-                      return-object
-                      :label="$t('actions.select_action')"
-                      item-text="name"
-                      outlined
-                      dense
-                      :hint="$tc('selection.num_items_selected', selectedItems.length)"
-                      persistent-hint
-                      append-outer-icon="$send"
-                      @click:append-outer="handleAction"
+                    auto-select-first
+                    clearable
+                    :items="generatedActions"
+                    v-model="selectedAction"
+                    return-object
+                    :label="$t('actions.select_action')"
+                    item-text="name"
+                    outlined
+                    dense
+                    :hint="
+                      $tc('selection.num_items_selected', selectedItems.length)
+                    "
+                    persistent-hint
+                    append-outer-icon="$send"
+                    @click:append-outer="handleAction"
                   >
                     <template #item="{ item, attrs, on }">
-                      <v-list-item
-                          dense
-                          v-bind="attrs"
-                          v-on="on"
-                      >
+                      <v-list-item dense v-bind="attrs" v-on="on">
                         <v-list-item-icon v-if="item.icon">
-                          <v-icon v-text="item.icon"/>
+                          <v-icon v-text="item.icon" />
                         </v-list-item-icon>
                         <v-list-item-content>
-                          <v-list-item-title v-text="item.name"></v-list-item-title>
+                          <v-list-item-title
+                            v-text="item.name"
+                          ></v-list-item-title>
                         </v-list-item-content>
                       </v-list-item>
                     </template>
@@ -121,80 +118,79 @@
                 </div>
               </v-toolbar-title>
 
-                <v-spacer class="flex-grow-0 flex-sm-grow-1 mx-n1 mx-sm-0"></v-spacer>
-                <slot
-                    v-if="!editMode && showCreate"
-                    name="createComponent"
-                    :attrs='{
-                        value: createMode,
-                        getCreateData: getCreateData,
-                        defaultItem: defaultItem,
-                        gqlQuery: gqlQuery,
-                        gqlCreateMutation: gqlCreateMutation,
-                        gqlPatchMutation: gqlPatchMutation,
-                        isCreate: true,
-                        fields: editableHeaders,
-                        createItemI18nKey: createItemI18nKey,
-                    }'
-
-                    :on="{
-                      input: i => i ? requestCreate() : null,
-                      cancel: cancelCreate,
-                      save: handleCreateDone,
-                      error: handleError,
-                    }"
-
-                    :create-mode="createMode"
-                    :form-field-slot-name="formFieldSlotName"
-
+              <v-spacer
+                class="flex-grow-0 flex-sm-grow-1 mx-n1 mx-sm-0"
+              ></v-spacer>
+              <slot
+                v-if="!editMode && showCreate"
+                name="createComponent"
+                :attrs="{
+                  value: createMode,
+                  getCreateData: getCreateData,
+                  defaultItem: defaultItem,
+                  gqlQuery: gqlQuery,
+                  gqlCreateMutation: gqlCreateMutation,
+                  gqlPatchMutation: gqlPatchMutation,
+                  isCreate: true,
+                  fields: editableHeaders,
+                  createItemI18nKey: createItemI18nKey,
+                }"
+                :on="{
+                  input: (i) => (i ? requestCreate() : null),
+                  cancel: cancelCreate,
+                  save: handleCreateDone,
+                  error: handleError,
+                }"
+                :create-mode="createMode"
+                :form-field-slot-name="formFieldSlotName"
+              >
+                <dialog-object-form
+                  v-model="createMode"
+                  :get-create-data="getCreateData"
+                  :default-item="defaultItem"
+                  :gql-create-mutation="gqlCreateMutation"
+                  :gql-patch-mutation="gqlPatchMutation"
+                  :isCreate="true"
+                  :fields="editableHeaders"
+                  :create-item-i18n-key="createItemI18nKey"
+                  @cancel="cancelCreate"
+                  @save="handleCreateDone"
+                  @error="handleError"
                 >
-                  <dialog-object-form
-                    v-model="createMode"
-                    :get-create-data="getCreateData"
-                    :default-item="defaultItem"
-                    :gql-create-mutation="gqlCreateMutation"
-                    :gql-patch-mutation="gqlPatchMutation"
-                    :isCreate="true"
-                    :fields="editableHeaders"
-                    :create-item-i18n-key="createItemI18nKey"
-                    @cancel="cancelCreate"
-                    @save="handleCreateDone"
-                    @error="handleError"
-                  >
-                    <template #activator="{ props }">
-                      <create-button
-                        color="secondary"
-                        @click="requestCreate"
-                        :disabled="createMode"
-                      />
-                    </template>
+                  <template #activator="{ props }">
+                    <create-button
+                      color="secondary"
+                      @click="requestCreate"
+                      :disabled="createMode"
+                    />
+                  </template>
 
-                    <template
-                      v-for="header in editableHeaders"
-                      #[formFieldSlotName(header)]="{ item, isCreate, on, attrs }"
-                    >
-                      <slot
-                        :name="formFieldSlotName(header)"
-                        :attrs="attrs"
-                        :on="on"
-                        :item="item"
-                        :isCreate="isCreate"
-                      />
-                    </template>
-                  </dialog-object-form>
-                </slot>
-                <edit-button
-                  v-if="!editMode && editingEnabled"
-                  @click="requestEdit"
-                  :disabled="createMode"
-                />
-                <cancel-button v-if="editMode" @click="cancelEdit" />
-                <save-button
-                  v-if="editMode"
-                  @click="saveEdit"
-                  :loading="loading"
-                  :disabled="!valid"
-                />
+                  <template
+                    v-for="header in editableHeaders"
+                    #[formFieldSlotName(header)]="{ item, isCreate, on, attrs }"
+                  >
+                    <slot
+                      :name="formFieldSlotName(header)"
+                      :attrs="attrs"
+                      :on="on"
+                      :item="item"
+                      :isCreate="isCreate"
+                    />
+                  </template>
+                </dialog-object-form>
+              </slot>
+              <edit-button
+                v-if="!editMode && editingEnabled"
+                @click="requestEdit"
+                :disabled="createMode"
+              />
+              <cancel-button v-if="editMode" @click="cancelEdit" />
+              <save-button
+                v-if="editMode"
+                @click="saveEdit"
+                :loading="loading"
+                :disabled="!valid"
+              />
             </v-row>
           </v-toolbar>
         </template>
@@ -229,10 +225,7 @@
         </template>
 
         <template #item.actions="{ item }">
-          <slot
-            name="actions"
-            :item="item"
-          />
+          <slot name="actions" :item="item" />
           <v-btn
             v-if="'canDelete' in item && item.canDelete"
             icon
@@ -287,13 +280,15 @@ export default {
           return {
             orderBy: this.gqlOrderBy,
             filters: this.filterString,
-          }
+          };
         },
         error: function (error) {
           this.handleError(error);
         },
         result: function (data) {
-          this.editableItems = data.data ? JSON.parse(JSON.stringify(data.data.items)) : [];
+          this.editableItems = data.data
+            ? JSON.parse(JSON.stringify(data.data.items))
+            : [];
         },
       };
     },
@@ -328,14 +323,18 @@ export default {
   computed: {
     tableHeaders() {
       return this.headers
-        .concat(this.deletionEnabled ? [
-          {
-            text: this.$t("actions.title"),
-            value: "actions",
-            sortable: false,
-            align: "right",
-          },
-        ] : [])
+        .concat(
+          this.deletionEnabled
+            ? [
+                {
+                  text: this.$t("actions.title"),
+                  value: "actions",
+                  sortable: false,
+                  align: "right",
+                },
+              ]
+            : []
+        )
         .filter((header) => this.hiddenColumns.indexOf(header.value) === -1);
     },
     editableHeaders() {
@@ -345,13 +344,24 @@ export default {
       return this.elevated ? "elevation-2" : "";
     },
     editingEnabled() {
-      return this.gqlPatchMutation && this.items && this.items.some(i => i.canEdit);
+      return (
+        this.gqlPatchMutation && this.items && this.items.some((i) => i.canEdit)
+      );
     },
     deletionEnabled() {
-      return this.gqlDeleteMutation && this.items && this.items.some(i => i.canDelete);
+      return (
+        this.gqlDeleteMutation &&
+        this.items &&
+        this.items.some((i) => i.canDelete)
+      );
     },
     multipleDeletionEnabled() {
-      return this.multipleDeletion && this.gqlDeleteMultipleMutation && this.items && this.items.some(i => i.canDelete);
+      return (
+        this.multipleDeletion &&
+        this.gqlDeleteMultipleMutation &&
+        this.items &&
+        this.items.some((i) => i.canDelete)
+      );
     },
     numFilters() {
       // This needs to use the json string, as vue reactivity doesn't work for objects with dynamic properties
@@ -366,14 +376,14 @@ export default {
         {
           name: this.$t("actions.delete"),
           icon: "$deleteContent",
-          handler: items => {
+          handler: (items) => {
             this.itemsToDelete = items;
             this.multipleDeletionDialog = true;
           },
           clearSelection: true,
-        }
-      ]
-    }
+        },
+      ];
+    },
   },
   props: {
     i18nKey: {
@@ -472,7 +482,7 @@ export default {
       type: Boolean,
       required: false,
       default: true,
-    }
+    },
   },
   methods: {
     requestCreate() {
@@ -624,21 +634,24 @@ export default {
       };
     },
     snakeCase(string) {
-      return string.replace(/\W+/g, " ")
-          .split(/ |\B(?=[A-Z])/)
-          .map(word => word.toLowerCase())
-          .join('_');
+      return string
+        .replace(/\W+/g, " ")
+        .split(/ |\B(?=[A-Z])/)
+        .map((word) => word.toLowerCase())
+        .join("_");
     },
     orderKey(value, desc) {
-      const key = this.headers.find(header => header.value === value).orderKey || this.snakeCase(value);
+      const key =
+        this.headers.find((header) => header.value === value).orderKey ||
+        this.snakeCase(value);
       return (desc ? "-" : "") + key;
     },
     handleSortChange() {
-      this.gqlOrderBy = this.sortBy.map(
-          (value, key) => this.orderKey(value, this.sortDesc[key])
-      )
+      this.gqlOrderBy = this.sortBy.map((value, key) =>
+        this.orderKey(value, this.sortDesc[key])
+      );
     },
-    handleItemSelected({item, value}) {
+    handleItemSelected({ item, value }) {
       if (value) {
         this.selectedItems.push(item);
       } else {
@@ -648,10 +661,10 @@ export default {
         }
       }
     },
-    handleToggleAll({items, value}) {
+    handleToggleAll({ items, value }) {
       if (value) {
         // There is a bug in vuetify: items contains all elements, even those that aren't selectable
-        this.selectedItems = items.filter(item => item.canDelete || false);
+        this.selectedItems = items.filter((item) => item.canDelete || false);
       } else {
         this.selectedItems = [];
       }
@@ -659,12 +672,10 @@ export default {
     },
     checkSelectAll(newItems) {
       if (this.allSelected) {
-        this.handleToggleAll(
-            {
-            items: newItems,
-            value: true,
-          }
-        )
+        this.handleToggleAll({
+          items: newItems,
+          value: true,
+        });
       }
     },
     handleAction() {
@@ -689,7 +700,8 @@ export default {
 .gap {
   gap: 0.5rem;
 }
-.height-fit, .child-height-fit > * {
+.height-fit,
+.child-height-fit > * {
   height: fit-content !important;
 }
 
diff --git a/aleksis/core/frontend/components/generic/ObjectCRUDList.vue b/aleksis/core/frontend/components/generic/ObjectCRUDList.vue
index 99fd458ab48a9516c5c4727083dba6438c87947e..c3cd91d4c2a5002de18cc5802cff5e1ddc50584a 100644
--- a/aleksis/core/frontend/components/generic/ObjectCRUDList.vue
+++ b/aleksis/core/frontend/components/generic/ObjectCRUDList.vue
@@ -13,7 +13,9 @@ import DialogObjectForm from "./dialogs/DialogObjectForm.vue";
     >
       <template #loading>
         <slot name="loading">
-          <v-skeleton-loader type="card-heading, list-item-avatar-two-line@3, actions" />
+          <v-skeleton-loader
+            type="card-heading, list-item-avatar-two-line@3, actions"
+          />
         </slot>
       </template>
 
@@ -37,24 +39,38 @@ import DialogObjectForm from "./dialogs/DialogObjectForm.vue";
         >
           <v-list>
             <template v-for="(item, index) in items">
-              <v-list-item
-                :key="item.id"
-              >
+              <v-list-item :key="item.id">
                 <v-list-item-avatar>
-                  <slot name="listIteratorItemAvatar" :item="item" :index="index" />
+                  <slot
+                    name="listIteratorItemAvatar"
+                    :item="item"
+                    :index="index"
+                  />
                 </v-list-item-avatar>
                 <v-list-item-content>
-                  <slot name="listIteratorItemContent" :item="item" :index="index">
+                  <slot
+                    name="listIteratorItemContent"
+                    :item="item"
+                    :index="index"
+                  >
                     <v-list-item-title>
                       {{ item.name }}
                     </v-list-item-title>
                   </slot>
                 </v-list-item-content>
                 <v-list-item-action>
-                  <v-btn v-if="editingEnabled && item.canEdit" icon @click="handleEdit(item)">
+                  <v-btn
+                    v-if="editingEnabled && item.canEdit"
+                    icon
+                    @click="handleEdit(item)"
+                  >
                     <v-icon>mdi-pencil-outline</v-icon>
                   </v-btn>
-                  <v-btn v-if="deletionEnabled && item.canDelete" icon @click="handleDelete(item)">
+                  <v-btn
+                    v-if="deletionEnabled && item.canDelete"
+                    icon
+                    @click="handleDelete(item)"
+                  >
                     <v-icon>mdi-delete-outline</v-icon>
                   </v-btn>
                 </v-list-item-action>
@@ -214,7 +230,7 @@ export default {
       type: Number,
       required: false,
       default: 5,
-    }
+    },
   },
   components: {
     CreateButton,
@@ -239,11 +255,11 @@ export default {
         error: function (error) {
           this.handleError(error);
         },
-        update (data) {
+        update(data) {
           return this.getGqlData(data);
         },
-      }
-    }
+      };
+    },
   },
   methods: {
     handleCreate() {
@@ -260,9 +276,7 @@ export default {
       this.isCreate = false;
       this.objectFormModel = true;
     },
-    handleDelete() {
-
-    },
+    handleDelete() {},
     handleCreateDone() {
       this.$apollo.queries.items.refetch();
     },
@@ -290,11 +304,7 @@ export default {
   },
   computed: {
     creatingEnabled() {
-      return (
-        this.gqlCreateMutation &&
-        this.fields &&
-        this.defaultItem
-      );
+      return this.gqlCreateMutation && this.fields && this.defaultItem;
     },
     editingEnabled() {
       return (
@@ -313,7 +323,7 @@ export default {
     },
     title() {
       return this.titleI18nKey ? this.$t(this.titleI18nKey) : this.titleString;
-    }
+    },
   },
 };
 </script>
diff --git a/aleksis/core/frontend/components/generic/buttons/BaseButton.vue b/aleksis/core/frontend/components/generic/buttons/BaseButton.vue
index 875b01b4105c1223b7011ad203af10f39adb2798..22d6a64c334ad78285d88e90f7767b43a6fdf35f 100644
--- a/aleksis/core/frontend/components/generic/buttons/BaseButton.vue
+++ b/aleksis/core/frontend/components/generic/buttons/BaseButton.vue
@@ -1,8 +1,8 @@
 <template>
   <v-btn v-bind="{ ...$props, ...$attrs }" @click="$emit('click', $event)">
     <slot>
-      <v-icon v-if="iconText" v-text="iconText" left/>
-      <span v-t="i18nKey"/>
+      <v-icon v-if="iconText" v-text="iconText" left />
+      <span v-t="i18nKey" />
     </slot>
   </v-btn>
 </template>
diff --git a/aleksis/core/frontend/components/generic/buttons/FilterButton.vue b/aleksis/core/frontend/components/generic/buttons/FilterButton.vue
index b379d1d1a4e744ddfd3b657dd6cdb74695002e9e..ea8aa88edcbb23a7283c650eed7beb5d0a22c05b 100644
--- a/aleksis/core/frontend/components/generic/buttons/FilterButton.vue
+++ b/aleksis/core/frontend/components/generic/buttons/FilterButton.vue
@@ -1,29 +1,23 @@
 <template>
   <secondary-action-button
-      v-bind="$attrs"
-      v-on="$listeners"
-      :i18n-key="i18nKey"
+    v-bind="$attrs"
+    v-on="$listeners"
+    :i18n-key="i18nKey"
   >
-    <v-icon v-text="icon" left/>
-    <v-badge
-        color="secondary"
-        :value="numFilters"
-        :content="numFilters"
-        inline
-    >
-      <span v-t="i18nKey"/>
+    <v-icon v-text="icon" left />
+    <v-badge color="secondary" :value="numFilters" :content="numFilters" inline>
+      <span v-t="i18nKey" />
     </v-badge>
     <v-btn
-        icon
-        @click.stop="$emit('clear')"
-        small
-        v-if="numFilters"
-        class="mr-n1"
+      icon
+      @click.stop="$emit('clear')"
+      small
+      v-if="numFilters"
+      class="mr-n1"
     >
       <v-icon>$clear</v-icon>
     </v-btn>
   </secondary-action-button>
-
 </template>
 
 <script>
@@ -31,12 +25,14 @@ import SecondaryActionButton from "./SecondaryActionButton.vue";
 
 export default {
   name: "FilterButton",
-  components: {SecondaryActionButton},
+  components: { SecondaryActionButton },
   extends: SecondaryActionButton,
   computed: {
     icon() {
-      return this.hasFilters || this.numFilters > 0 ? "$filterSet" : "$filterEmpty";
-    }
+      return this.hasFilters || this.numFilters > 0
+        ? "$filterSet"
+        : "$filterEmpty";
+    },
   },
   props: {
     i18nKey: {
@@ -53,7 +49,7 @@ export default {
       type: Number,
       required: false,
       default: 0,
-    }
+    },
   },
 };
 </script>
diff --git a/aleksis/core/frontend/components/generic/dialogs/DeleteDialog.vue b/aleksis/core/frontend/components/generic/dialogs/DeleteDialog.vue
index 97f3c87f3098b55b145aed8d4d8a0e01a7a0d681..550ca6f9669ad49f47d7efa587a30c2444846fc8 100644
--- a/aleksis/core/frontend/components/generic/dialogs/DeleteDialog.vue
+++ b/aleksis/core/frontend/components/generic/dialogs/DeleteDialog.vue
@@ -1,6 +1,6 @@
 <script setup>
 import CancelButton from "../buttons/CancelButton.vue";
-import DeleteButton from "../buttons/DeleteButton.vue"
+import DeleteButton from "../buttons/DeleteButton.vue";
 import MobileFullscreenDialog from "./MobileFullscreenDialog.vue";
 import PrimaryActionButton from "../buttons/PrimaryActionButton.vue";
 </script>
@@ -15,31 +15,27 @@ import PrimaryActionButton from "../buttons/PrimaryActionButton.vue";
   >
     <template #default="{ mutate, loading, error }">
       <mobile-fullscreen-dialog v-model="dialogOpen">
-          <template #title>
-            <slot name="title">
-              {{ $t("actions.confirm_deletion") }}
-            </slot>
-          </template>
-          <template #content>
-            <slot name="body">
-              <p class="text-body-1">{{ nameOfObject }}</p>
+        <template #title>
+          <slot name="title">
+            {{ $t("actions.confirm_deletion") }}
+          </slot>
+        </template>
+        <template #content>
+          <slot name="body">
+            <p class="text-body-1">{{ nameOfObject }}</p>
+          </slot>
+        </template>
+        <template #actions>
+          <cancel-button @click="close(false)" :disabled="loading">
+            <slot name="cancelContent">
+              <v-icon left>$cancel</v-icon>
+              {{ $t("actions.cancel") }}
             </slot>
-          </template>
-          <template #actions>
-            <cancel-button @click="close(false)" :disabled="loading">
-              <slot name="cancelContent">
-                <v-icon left>$cancel</v-icon>
-                {{ $t("actions.cancel") }}
-              </slot>
-            </cancel-button>
-            <delete-button
-              @click="mutate"
-              :loading="loading"
-              :disabled="loading"
-            >
-              <slot name="deleteContent" />
-            </delete-button>
-          </template>
+          </cancel-button>
+          <delete-button @click="mutate" :loading="loading" :disabled="loading">
+            <slot name="deleteContent" />
+          </delete-button>
+        </template>
       </mobile-fullscreen-dialog>
       <v-snackbar :value="error !== null">
         {{ error }}
@@ -76,11 +72,11 @@ export default {
       if (this.gqlQuery.hasOwnProperty("options")) {
         return {
           ...this.gqlQuery.options,
-          variables: JSON.parse(this.gqlQuery.previousVariablesJson)
-        }
+          variables: JSON.parse(this.gqlQuery.previousVariablesJson),
+        };
       }
-      return {query: this.gqlQuery}
-    }
+      return { query: this.gqlQuery };
+    },
   },
   methods: {
     update(store) {
diff --git a/aleksis/core/frontend/components/generic/dialogs/DeleteMultipleDialog.vue b/aleksis/core/frontend/components/generic/dialogs/DeleteMultipleDialog.vue
index b1842ea8f60592801863765b80585a7ab8776b8b..6e80c26d98796af5a66309f6502b03334f0261b9 100644
--- a/aleksis/core/frontend/components/generic/dialogs/DeleteMultipleDialog.vue
+++ b/aleksis/core/frontend/components/generic/dialogs/DeleteMultipleDialog.vue
@@ -1,6 +1,6 @@
 <script setup>
 import CancelButton from "../buttons/CancelButton.vue";
-import DeleteButton from "../buttons/DeleteButton.vue"
+import DeleteButton from "../buttons/DeleteButton.vue";
 import MobileFullscreenDialog from "./MobileFullscreenDialog.vue";
 </script>
 
@@ -14,36 +14,29 @@ import MobileFullscreenDialog from "./MobileFullscreenDialog.vue";
   >
     <template #default="{ mutate, loading, error }">
       <mobile-fullscreen-dialog v-model="dialogOpen">
-          <template #title>
-            <slot name="title">
-              {{ $t("actions.confirm_deletion_multiple") }}
+        <template #title>
+          <slot name="title">
+            {{ $t("actions.confirm_deletion_multiple") }}
+          </slot>
+        </template>
+        <template #content>
+          <slot name="body">
+            <ul class="text-body-1">
+              <li v-for="item in items">{{ nameOfItem(item) }}</li>
+            </ul>
+          </slot>
+        </template>
+        <template #actions>
+          <cancel-button @click="close(false)" :disabled="loading">
+            <slot name="cancelContent">
+              <v-icon left>$cancel</v-icon>
+              {{ $t("actions.cancel") }}
             </slot>
-          </template>
-          <template #content>
-            <slot name="body">
-              <ul class="text-body-1">
-                <li v-for="item in items">{{ nameOfItem(item) }}</li>
-              </ul>
-            </slot>
-          </template>
-          <template #actions>
-            <cancel-button
-                @click="close(false)"
-                :disabled="loading"
-            >
-              <slot name="cancelContent">
-                <v-icon left>$cancel</v-icon>
-                {{ $t("actions.cancel") }}
-              </slot>
-            </cancel-button>
-            <delete-button
-              @click="mutate"
-              :loading="loading"
-              :disabled="loading"
-            >
-              <slot name="deleteContent" />
-            </delete-button>
-          </template>
+          </cancel-button>
+          <delete-button @click="mutate" :loading="loading" :disabled="loading">
+            <slot name="deleteContent" />
+          </delete-button>
+        </template>
       </mobile-fullscreen-dialog>
     </template>
   </ApolloMutation>
@@ -63,17 +56,17 @@ export default {
       },
     },
     ids() {
-      return this.items.map(item => item[this.itemId]);
+      return this.items.map((item) => item[this.itemId]);
     },
     query() {
       if (this.gqlQuery.hasOwnProperty("options")) {
         return {
           ...this.gqlQuery.options,
-          variables: JSON.parse(this.gqlQuery.previousVariablesJson)
-        }
+          variables: JSON.parse(this.gqlQuery.previousVariablesJson),
+        };
       }
-      return {query: this.gqlQuery}
-    }
+      return { query: this.gqlQuery };
+    },
   },
   methods: {
     update(store) {
@@ -98,7 +91,7 @@ export default {
         console.debug("Removing item from store:", item);
         // Remove item from stored data
         const index = storedData[storedDataKey].findIndex(
-            (m) => m.id === item.id
+          (m) => m.id === item.id
         );
         storedData[storedDataKey].splice(index, 1);
       }
diff --git a/aleksis/core/frontend/components/generic/dialogs/DialogObjectForm.vue b/aleksis/core/frontend/components/generic/dialogs/DialogObjectForm.vue
index c1cdf384ecddb025d7750fa590f21802e432b891..8d9bad8c7c3b6f9bbd384009406eb79e26311d51 100644
--- a/aleksis/core/frontend/components/generic/dialogs/DialogObjectForm.vue
+++ b/aleksis/core/frontend/components/generic/dialogs/DialogObjectForm.vue
@@ -156,7 +156,12 @@ export default {
     save() {
       this.loading = true;
 
-      if (!this.itemModel || this.isCreate && !this.gqlCreateMutation || !this.isCreate && !this.gqlPatchMutation) return;
+      if (
+        !this.itemModel ||
+        (this.isCreate && !this.gqlCreateMutation) ||
+        (!this.isCreate && !this.gqlPatchMutation)
+      )
+        return;
 
       let mutation = this.isCreate
         ? this.gqlCreateMutation
@@ -227,17 +232,19 @@ export default {
     updateModel() {
       // Only update the model if the dialog is hidden or has just been mounted
       if (this.forceModelItemUpdate || !this.firstInitDone || !this.dialog) {
-        this.itemModel = JSON.parse(JSON.stringify(this.isCreate ? this.defaultItem : this.editItem));
+        this.itemModel = JSON.parse(
+          JSON.stringify(this.isCreate ? this.defaultItem : this.editItem)
+        );
       }
-    }
+    },
   },
   mounted() {
     this.updateModel();
     this.firstInitDone = true;
 
     this.$watch("isCreate", this.updateModel);
-    this.$watch("defaultItem", this.updateModel, { deep: true, });
-    this.$watch("editItem", this.updateModel, { deep: true, });
+    this.$watch("defaultItem", this.updateModel, { deep: true });
+    this.$watch("editItem", this.updateModel, { deep: true });
   },
 };
 </script>
diff --git a/aleksis/core/frontend/components/generic/dialogs/FilterDialog.vue b/aleksis/core/frontend/components/generic/dialogs/FilterDialog.vue
index f8b75641b871060b24bcd550d6c8150cdbf03bcf..f1ead340b44ca0982cf0726607146a08f1c57c42 100644
--- a/aleksis/core/frontend/components/generic/dialogs/FilterDialog.vue
+++ b/aleksis/core/frontend/components/generic/dialogs/FilterDialog.vue
@@ -1,22 +1,23 @@
 <template>
-  <mobile-fullscreen-dialog
-      v-bind="$attrs"
-      v-on="$listeners"
-  >
+  <mobile-fullscreen-dialog v-bind="$attrs" v-on="$listeners">
     <template #title>{{ $t("actions.filter") }}</template>
 
     <template #content>
       <form ref="form" @submit.prevent="save">
-        <slot
-            :attrs="attrs"
-            :on="on"
-        ></slot>
+        <slot :attrs="attrs" :on="on"></slot>
       </form>
     </template>
 
     <template #actions>
-      <cancel-button i18n-key="actions.clear_filters" @click="clearFilters"></cancel-button>
-      <save-button i18n-key="actions.filter" icon-text="$filterEmpty" @click="save"></save-button>
+      <cancel-button
+        i18n-key="actions.clear_filters"
+        @click="clearFilters"
+      ></cancel-button>
+      <save-button
+        i18n-key="actions.filter"
+        icon-text="$filterEmpty"
+        @click="save"
+      ></save-button>
     </template>
   </mobile-fullscreen-dialog>
 </template>
@@ -28,12 +29,12 @@ import SaveButton from "../buttons/SaveButton.vue";
 
 export default {
   name: "FilterDialog",
-  components: {SaveButton, CancelButton, MobileFullscreenDialog},
+  components: { SaveButton, CancelButton, MobileFullscreenDialog },
   props: {
     filters: {
       type: Object,
       required: true,
-    }
+    },
   },
   methods: {
     save() {
@@ -49,26 +50,25 @@ export default {
     },
     clearFilters() {
       this.$refs.form.reset();
-      this.$emit("filters", {})
+      this.$emit("filters", {});
       this.$emit("input", false);
     },
     on(field) {
       return {
-        change: i => this.filters[field] = i,
-        input: i => this.filters[field] = i
-      }
+        change: (i) => (this.filters[field] = i),
+        input: (i) => (this.filters[field] = i),
+      };
     },
     attrs(field, defaultValue) {
       if ([null, undefined].includes(this.filters[field]) && !!defaultValue) {
-        this.filters[field] = defaultValue
+        this.filters[field] = defaultValue;
       }
       return {
-        value: this.filters[field]
-      }
-    }
+        value: this.filters[field],
+      };
+    },
   },
-}
+};
 </script>
 
-<style scoped>
-</style>
+<style scoped></style>
diff --git a/aleksis/core/frontend/components/generic/forms/ColorField.vue b/aleksis/core/frontend/components/generic/forms/ColorField.vue
index d52c2b5187f6dc1d1254948a51d72924575d7d63..a2313cb9036a21e24922f2b171bd52788eb6a915 100644
--- a/aleksis/core/frontend/components/generic/forms/ColorField.vue
+++ b/aleksis/core/frontend/components/generic/forms/ColorField.vue
@@ -17,21 +17,11 @@
         :rules="rules"
       >
         <template #prepend-inner>
-          <v-icon
-            :color="color"
-            v-bind="attrs"
-            v-on="on"
-          >
-            mdi-circle
-          </v-icon>
+          <v-icon :color="color" v-bind="attrs" v-on="on"> mdi-circle </v-icon>
         </template>
       </v-text-field>
     </template>
-    <v-color-picker
-      v-if="menu"
-      v-model="color"
-      ref="picker"
-    ></v-color-picker>
+    <v-color-picker v-if="menu" v-model="color" ref="picker"></v-color-picker>
   </v-menu>
 </template>
 
@@ -43,14 +33,16 @@ export default {
     return {
       menu: false,
       rules: [
-        value => /^(#([0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8}))?$/i.test(value) || this.$t("forms.errors.invalid_color"),
+        (value) =>
+          /^(#([0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8}))?$/i.test(value) ||
+          this.$t("forms.errors.invalid_color"),
       ],
-    }
+    };
   },
   props: {
     value: {
       type: String,
-      default: undefined
+      default: undefined,
     },
   },
   computed: {
@@ -60,10 +52,10 @@ export default {
       },
       set(newValue) {
         this.$emit("input", newValue);
-      }
-    }
+      },
+    },
   },
-}
+};
 </script>
 
 <style scoped></style>
diff --git a/aleksis/core/frontend/components/generic/forms/DateField.vue b/aleksis/core/frontend/components/generic/forms/DateField.vue
index fded64cafc47d622be4d71a06fa9829fccba8f40..0ac11bc4b7a4c7c898ec7fc5a40c99eb73f12ce1 100644
--- a/aleksis/core/frontend/components/generic/forms/DateField.vue
+++ b/aleksis/core/frontend/components/generic/forms/DateField.vue
@@ -47,7 +47,8 @@ export default {
       innerDate: this.value,
       openDueToFocus: true,
       rules: [
-        (value) => !value || !!Date.parse(value) || this.$t("forms.errors.invalid_date"),
+        (value) =>
+          !value || !!Date.parse(value) || this.$t("forms.errors.invalid_date"),
       ],
     };
   },
diff --git a/aleksis/core/frontend/components/generic/forms/ForeignKeyField.vue b/aleksis/core/frontend/components/generic/forms/ForeignKeyField.vue
index 4da1ab6b273b129b0100b9ca5066370dbaa5887b..62b93382be643f67840e458eddb78f6404fce151 100644
--- a/aleksis/core/frontend/components/generic/forms/ForeignKeyField.vue
+++ b/aleksis/core/frontend/components/generic/forms/ForeignKeyField.vue
@@ -1,11 +1,11 @@
 <template>
   <v-autocomplete
-      v-bind="$attrs"
-      v-on="$listeners"
-      :items="items"
-      item-value="id"
-      :item-text="itemName"
-      class="fc-my-auto"
+    v-bind="$attrs"
+    v-on="$listeners"
+    :items="items"
+    item-value="id"
+    :item-text="itemName"
+    class="fc-my-auto"
   >
     <template #append-outer>
       <v-btn icon @click="menu = true">
@@ -14,7 +14,7 @@
 
       <slot
         name="createComponent"
-        :attrs='{
+        :attrs="{
           value: menu,
           defaultItem: defaultItem,
           gqlQuery: gqlQuery,
@@ -24,11 +24,10 @@
           fields: fields,
           getCreateData: getCreateData,
           createItemI18nKey: createItemI18nKey,
-        }'
-
+        }"
         :on="{
-          input: i => menu = i,
-          cancel: () => menu = false,
+          input: (i) => (menu = i),
+          cancel: () => (menu = false),
           save: handleSave,
           update: handleUpdate,
         }"
@@ -48,16 +47,17 @@
           :create-item-i18n-key="createItemI18nKey"
           :get-create-data="getCreateData"
         >
-          <template v-for="(_, name) in $scopedSlots" :slot="name" slot-scope="slotData">
-            <slot :name="name" v-bind="slotData"/>
+          <template
+            v-for="(_, name) in $scopedSlots"
+            :slot="name"
+            slot-scope="slotData"
+          >
+            <slot :name="name" v-bind="slotData" />
           </template>
         </dialog-object-form>
       </slot>
 
-      <closeable-snackbar
-          :color="snackbarState"
-          v-model="snackbar"
-      >
+      <closeable-snackbar :color="snackbarState" v-model="snackbar">
         {{ snackbarText }}
       </closeable-snackbar>
     </template>
@@ -70,7 +70,7 @@ import DialogObjectForm from "../dialogs/DialogObjectForm.vue";
 
 export default {
   name: "ForeignKeyField",
-  components: {CloseableSnackbar, DialogObjectForm},
+  components: { CloseableSnackbar, DialogObjectForm },
   extends: "v-autocomplete",
   data() {
     return {
@@ -78,7 +78,7 @@ export default {
       snackbar: false,
       snackbarState: "error",
       snackbarText: "",
-    }
+    };
   },
   apollo: {
     items() {
@@ -91,7 +91,7 @@ export default {
   methods: {
     handleUpdate(store, createdObject) {
       // Read the data from cache for query
-      const storedData = store.readQuery({query: this.gqlQuery});
+      const storedData = store.readQuery({ query: this.gqlQuery });
 
       if (!storedData) {
         // There are no data in the cache yet
@@ -104,7 +104,7 @@ export default {
       storedData[storedDataKey].push(createdObject);
 
       // Write data back to the cache
-      store.writeQuery({query: this.gqlQuery, data: storedData});
+      store.writeQuery({ query: this.gqlQuery, data: storedData });
     },
     handleSave(data) {
       let newItem =
@@ -172,7 +172,7 @@ export default {
       default: "actions.create",
     },
   },
-}
+};
 </script>
 
 <style scoped>
diff --git a/aleksis/core/frontend/components/generic/forms/PositiveSmallIntegerField.vue b/aleksis/core/frontend/components/generic/forms/PositiveSmallIntegerField.vue
index e225f2abc6e21db9f78569ea9a701564768be46a..5deaab572366481ee22ff147028bfb32a79e00eb 100644
--- a/aleksis/core/frontend/components/generic/forms/PositiveSmallIntegerField.vue
+++ b/aleksis/core/frontend/components/generic/forms/PositiveSmallIntegerField.vue
@@ -1,10 +1,10 @@
 <template>
   <v-text-field
-      v-bind="$attrs"
-      v-on="on"
-      :rules="rules"
-      type="number"
-      inputmode="decimal"
+    v-bind="$attrs"
+    v-on="on"
+    :rules="rules"
+    type="number"
+    inputmode="decimal"
   ></v-text-field>
 </template>
 
@@ -18,29 +18,39 @@ export default {
       if (!isNaN(num) && num >= 0 && num <= 32767 && num % 1 === 0) {
         this.$emit("input", parseInt(event));
       }
-    }
+    },
   },
   data() {
     return {
       rules: [
-          value => !value || !isNaN(parseInt(value)) || this.$t("forms.errors.not_a_number"),
-          value => !value || value % 1 === 0 || this.$t("forms.errors.not_a_whole_number"),
-          value => !value || parseInt(value) >= 0 || this.$t("forms.errors.number_too_small"),
-          value => !value || parseInt(value) <= 32767 || this.$t("forms.errors.number_too_big"),
-      ]
-    }
+        (value) =>
+          !value ||
+          !isNaN(parseInt(value)) ||
+          this.$t("forms.errors.not_a_number"),
+        (value) =>
+          !value ||
+          value % 1 === 0 ||
+          this.$t("forms.errors.not_a_whole_number"),
+        (value) =>
+          !value ||
+          parseInt(value) >= 0 ||
+          this.$t("forms.errors.number_too_small"),
+        (value) =>
+          !value ||
+          parseInt(value) <= 32767 ||
+          this.$t("forms.errors.number_too_big"),
+      ],
+    };
   },
   computed: {
     on() {
       return {
         ...this.$listeners,
         input: this.handleInput,
-      }
-    }
+      };
+    },
   },
-}
+};
 </script>
 
-<style scoped>
-
-</style>
+<style scoped></style>
diff --git a/aleksis/core/frontend/components/generic/forms/WeekDayField.vue b/aleksis/core/frontend/components/generic/forms/WeekDayField.vue
index 3103faa2e4019266dc4882f9c1642bacedc46479..4e8c359343f799c43974ea923a27c8648bfa314c 100644
--- a/aleksis/core/frontend/components/generic/forms/WeekDayField.vue
+++ b/aleksis/core/frontend/components/generic/forms/WeekDayField.vue
@@ -1,14 +1,13 @@
 <template>
   <v-autocomplete
-      v-bind="$attrs"
-      v-on="$listeners"
-      :items="items"
-      :item-value="valueKey"
+    v-bind="$attrs"
+    v-on="$listeners"
+    :items="items"
+    :item-value="valueKey"
   ></v-autocomplete>
 </template>
 
 <script>
-
 export default {
   name: "WeekDayField",
   extends: "v-autocomplete",
@@ -50,20 +49,20 @@ export default {
           valueInt: 6,
           text: this.$t("weekdays.A_6"),
         },
-      ]
-    }
+      ],
+    };
   },
   props: {
     returnInt: {
       type: Boolean,
       default: false,
       required: false,
-    }
+    },
   },
   computed: {
     valueKey() {
       return this.returnInt ? "valueInt" : "value";
-    }
+    },
   },
-}
+};
 </script>
diff --git a/aleksis/core/frontend/components/notifications/NotificationList.vue b/aleksis/core/frontend/components/notifications/NotificationList.vue
index 1c51c00661816fe3e244e6d511a0e70c747dbca6..09b27197e5d62c5a5f15f7836d5b741925d21cc8 100644
--- a/aleksis/core/frontend/components/notifications/NotificationList.vue
+++ b/aleksis/core/frontend/components/notifications/NotificationList.vue
@@ -20,7 +20,7 @@
           v-if="
             myNotifications &&
             myNotifications.person &&
-            myNotifications.person.unreadNotificationsCount > 0
+            myNotifications.person.notifications.length > 0
           "
         >
           mdi-bell-badge-outline
@@ -38,7 +38,7 @@
         v-if="
           myNotifications.person &&
           myNotifications.person.notifications &&
-          myNotifications.person.notifications.length
+          unreadNotifications.length
         "
       >
         <v-subheader>{{ $t("notifications.notifications") }}</v-subheader>
@@ -86,5 +86,10 @@ export default {
       pollInterval: 30000,
     },
   },
+  computed: {
+    unreadNotifications() {
+      return this.myNotifications.filter((n) => !n.read);
+    },
+  },
 };
 </script>
diff --git a/aleksis/core/frontend/components/notifications/myNotifications.graphql b/aleksis/core/frontend/components/notifications/myNotifications.graphql
index b8287ea2f50664f556d82bdb58e4b508c7ece1d4..89e91562086607e6c9e7649fa1295d234d189bd3 100644
--- a/aleksis/core/frontend/components/notifications/myNotifications.graphql
+++ b/aleksis/core/frontend/components/notifications/myNotifications.graphql
@@ -1,7 +1,6 @@
 {
   myNotifications: whoAmI {
     person {
-      unreadNotificationsCount
       notifications {
         id
         title
diff --git a/aleksis/core/frontend/components/room/RoomInlineList.vue b/aleksis/core/frontend/components/room/RoomInlineList.vue
index 781414151b497be592b4a45c6ec6efecce338123..9221e83da03b65c43564f8584df226cbc9f27be0 100644
--- a/aleksis/core/frontend/components/room/RoomInlineList.vue
+++ b/aleksis/core/frontend/components/room/RoomInlineList.vue
@@ -28,7 +28,13 @@ import InlineCRUDList from "../generic/InlineCRUDList.vue";
 </template>
 
 <script>
-import { rooms, createRoom, deleteRoom, deleteRooms, updateRooms } from "./room.graphql";
+import {
+  rooms,
+  createRoom,
+  deleteRoom,
+  deleteRooms,
+  updateRooms,
+} from "./room.graphql";
 
 export default {
   name: "RoomInlineList",
diff --git a/aleksis/core/frontend/components/room/room.graphql b/aleksis/core/frontend/components/room/room.graphql
index d30034e90aff4620fef8a3bef58fe1ed0e7db938..d8cd9138dd0fa5c4f3af7a7d47fe7b44ed94f6fd 100644
--- a/aleksis/core/frontend/components/room/room.graphql
+++ b/aleksis/core/frontend/components/room/room.graphql
@@ -27,9 +27,9 @@ mutation deleteRoom($id: ID!) {
 }
 
 mutation deleteRooms($ids: [ID]!) {
-    deleteRooms(ids: $ids) {
-        deletionCount
-    }
+  deleteRooms(ids: $ids) {
+    deletionCount
+  }
 }
 
 mutation updateRooms($input: [BatchPatchRoomInput]!) {
diff --git a/aleksis/core/frontend/components/school_term/SchoolTermField.vue b/aleksis/core/frontend/components/school_term/SchoolTermField.vue
index 337bcfc0c1481eb9e5e727352c1902691d33fe71..d8b4c1b0491e0444ea5dad3edb2b88756e6954ea 100644
--- a/aleksis/core/frontend/components/school_term/SchoolTermField.vue
+++ b/aleksis/core/frontend/components/school_term/SchoolTermField.vue
@@ -4,14 +4,14 @@ import DateField from "../generic/forms/DateField.vue";
 
 <template>
   <foreign-key-field
-      :gql-patch-mutation="{}"
-      :gql-create-mutation="gqlCreateMutation"
-      :gql-query="gqlQuery"
-      :fields="fields"
-      create-item-i18n-key="school_term.create_school_term"
-      :default-item="defaultItem"
-      v-bind="$attrs"
-      v-on="$listeners"
+    :gql-patch-mutation="{}"
+    :gql-create-mutation="gqlCreateMutation"
+    :gql-query="gqlQuery"
+    :fields="fields"
+    create-item-i18n-key="school_term.create_school_term"
+    :default-item="defaultItem"
+    v-bind="$attrs"
+    v-on="$listeners"
   >
     <template #name.field="{ attrs, on }">
       <div aria-required="true">
@@ -58,12 +58,11 @@ import DateField from "../generic/forms/DateField.vue";
 
 <script>
 import ForeignKeyField from "../generic/forms/ForeignKeyField.vue";
-import {createSchoolTerm, schoolTerms,} from "./schoolTerm.graphql";
-
+import { createSchoolTerm, schoolTerms } from "./schoolTerm.graphql";
 
 export default {
   name: "SchoolTermField",
-  components: {ForeignKeyField},
+  components: { ForeignKeyField },
   data() {
     return {
       gqlQuery: schoolTerms,
@@ -88,11 +87,9 @@ export default {
         dateEnd: "",
       },
       required: [(value) => !!value || this.$t("forms.errors.required")],
-    }
+    };
   },
-}
+};
 </script>
 
-<style scoped>
-
-</style>
+<style scoped></style>
diff --git a/aleksis/core/frontend/components/school_term/SchoolTermInlineList.vue b/aleksis/core/frontend/components/school_term/SchoolTermInlineList.vue
index 1195ec8fe358876288e8083d2785d35e5a89a55a..c109f88e0c5213de7c4a86e31701f4b1276db74d 100644
--- a/aleksis/core/frontend/components/school_term/SchoolTermInlineList.vue
+++ b/aleksis/core/frontend/components/school_term/SchoolTermInlineList.vue
@@ -18,11 +18,7 @@ import DateField from "../generic/forms/DateField.vue";
   >
     <template #name.field="{ attrs, on, item }">
       <div aria-required="true">
-        <v-text-field
-          v-bind="attrs"
-          v-on="on"
-          :rules="required"
-        ></v-text-field>
+        <v-text-field v-bind="attrs" v-on="on" :rules="required"></v-text-field>
       </div>
     </template>
 
diff --git a/aleksis/core/frontend/plugins/aleksis.js b/aleksis/core/frontend/plugins/aleksis.js
index 2304404e7eb80c4150f07eeb38f888aaee88a60f..7354df14ecaeb5b640dca791dbdaaf3e3e460214 100644
--- a/aleksis/core/frontend/plugins/aleksis.js
+++ b/aleksis/core/frontend/plugins/aleksis.js
@@ -147,10 +147,11 @@ AleksisVue.install = function (Vue) {
    * Load vuetifys built-in translations
    */
   Vue.prototype.$loadVuetifyMessages = function () {
-    for(const [locale, messages] of Object.entries(langs)) {
-      this.$i18n.mergeLocaleMessage(locale, {$vuetify: messages})
+
+    for (const [locale, messages] of Object.entries(langs)) {
+      this.$i18n.mergeLocaleMessage(locale, { $vuetify: messages });
     }
-  }
+  };
 
   /**
    * Invalidate state and force reload from server.
diff --git a/aleksis/core/migrations/0049_oauthapplication_post_logout_redirect_uris.py b/aleksis/core/migrations/0049_oauthapplication_post_logout_redirect_uris.py
new file mode 100644
index 0000000000000000000000000000000000000000..a14526b2f0c32077e873c9b3bcd0cb21ee9e0cd6
--- /dev/null
+++ b/aleksis/core/migrations/0049_oauthapplication_post_logout_redirect_uris.py
@@ -0,0 +1,21 @@
+# Generated by Django 4.1.9 on 2023-06-17 10:59
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+    dependencies = [
+        ("sites", "0002_alter_domain_unique"),
+        ("core", "0048_delete_personalicalurl"),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name="oauthapplication",
+            name="post_logout_redirect_uris",
+            field=models.TextField(
+                blank=True, help_text="Allowed Post Logout URIs list, space separated"
+            ),
+        ),
+    ]
diff --git a/aleksis/core/schema/__init__.py b/aleksis/core/schema/__init__.py
index c5d5ddd502a2e8ebeb39c48288b94b05989c8d09..b59430c774aeff5f2513a1edaa41e56c962ffb9a 100644
--- a/aleksis/core/schema/__init__.py
+++ b/aleksis/core/schema/__init__.py
@@ -9,7 +9,6 @@ from haystack.inputs import AutoQuery
 from haystack.query import SearchQuerySet
 from haystack.utils.loading import UnifiedIndex
 
-from .base import FilterOrderList
 from ..models import (
     CustomMenu,
     DynamicRoute,
@@ -19,11 +18,11 @@ from ..models import (
     PDFFile,
     Person,
     Room,
-    SchoolTerm,
     TaskUserAssignment,
 )
 from ..util.apps import AppConfig
 from ..util.core_helpers import get_allowed_object_ids, get_app_module, get_app_packages, has_person
+from .base import FilterOrderList
 from .celery_progress import CeleryProgressFetchedMutation, CeleryProgressType
 from .custom_menu import CustomMenuType
 from .dynamic_routes import DynamicRouteType
@@ -34,12 +33,18 @@ from .notification import MarkNotificationReadMutation, NotificationType
 from .oauth import OAuthAccessTokenType, OAuthRevokeTokenMutation
 from .pdf import PDFFileType
 from .person import PersonMutation, PersonType
-from .room import RoomBatchDeleteMutation, RoomBatchPatchMutation, RoomCreateMutation, RoomDeleteMutation, RoomType
+from .room import (
+    RoomBatchDeleteMutation,
+    RoomBatchPatchMutation,
+    RoomCreateMutation,
+    RoomDeleteMutation,
+    RoomType,
+)
 from .school_term import (
+    SchoolTermBatchDeleteMutation,
     SchoolTermBatchPatchMutation,
     SchoolTermCreateMutation,
     SchoolTermDeleteMutation,
-    SchoolTermBatchDeleteMutation,
     SchoolTermType,
 )
 from .search import SearchResultType
diff --git a/aleksis/core/schema/base.py b/aleksis/core/schema/base.py
index 8084bf594c62ac19ef759ff393c1a3a75342ca1c..198217379e257e427aa1332ae7bce7734d139c18 100644
--- a/aleksis/core/schema/base.py
+++ b/aleksis/core/schema/base.py
@@ -3,9 +3,9 @@ import json
 from django.contrib.contenttypes.models import ContentType
 from django.core.exceptions import PermissionDenied
 from django.db.models import Model
-from django_filters.filterset import FilterSet, filterset_factory
 
 import graphene
+from django_filters.filterset import FilterSet, filterset_factory
 from graphene_django import DjangoListField, DjangoObjectType
 
 from ..util.core_helpers import queryset_rules_filter
@@ -82,7 +82,7 @@ class PermissionBatchPatchMixin:
         login_required = True
 
     @classmethod
-    def check_permissions(cls, root, info, input):
+    def check_permissions(cls, root, info, input):  # noqa
         if info.context.user.has_perms(cls._meta.permissions, root):
             return
 
@@ -94,7 +94,7 @@ class PermissionBatchDeleteMixin:
         login_required = True
 
     @classmethod
-    def check_permissions(cls, root, info, input):
+    def check_permissions(cls, root, info, input):  # noqa
         if info.context.user.has_perms(cls._meta.permissions, root):
             return
 
@@ -106,7 +106,7 @@ class PermissionPatchMixin:
         login_required = True
 
     @classmethod
-    def check_permissions(cls, root, info, input, id, obj):
+    def check_permissions(cls, root, info, input, id, obj):  # noqa
         if info.context.user.has_perms(cls._meta.permissions, root):
             return
 
@@ -136,21 +136,23 @@ class DjangoFilterMixin:
 
         if not fields:
             # Django filter doesn't allow to filter without explicit fields
-            raise NotImplementedError(f"{cls.__name__}.Meta must contain filter_fields or a filterset_class")
+            raise NotImplementedError(
+                f"{cls.__name__}.Meta must contain filter_fields or a filterset_class"
+            )
 
         fs = filterset_factory(model=model, fields=fields)
 
         return fs
 
     @classmethod
-    def filter(cls, filters, queryset):
+    def filter(cls, filters, queryset):  # noqa
         filterset_class = cls.get_filterset()
         filterset: FilterSet = filterset_class(filters, queryset)
         return filterset.qs
 
 
 class FilterOrderList(DjangoListField):
-    """Generic filterable Field for lists of django models
+    """Generic filterable Field for lists of django models.
 
     After the models are filtered, they can be filtered again (e.g.
     for permissions using the get_queryset method inside the
@@ -164,9 +166,18 @@ class FilterOrderList(DjangoListField):
 
     @staticmethod
     def list_resolver(
-        django_object_type, resolver, default_manager, root, info, order_by=None, filters=None, **args
+        django_object_type,
+        resolver,
+        default_manager,
+        root,
+        info,
+        order_by=None,
+        filters=None,
+        **args,
     ):
-        qs = DjangoListField.list_resolver(django_object_type, resolver, default_manager, root, info, **args)
+        qs = DjangoListField.list_resolver(
+            django_object_type, resolver, default_manager, root, info, **args
+        )
 
         if filters is not None:
             if isinstance(filters, str):
diff --git a/aleksis/core/schema/person.py b/aleksis/core/schema/person.py
index 6eed201487348034b2d9b1cba616046b1794dbf2..90165cceb0fb33676e17aad5caf978ce7c124be5 100644
--- a/aleksis/core/schema/person.py
+++ b/aleksis/core/schema/person.py
@@ -55,14 +55,26 @@ class PersonType(DjangoObjectType):
     full_name = graphene.String()
     username = graphene.String()
     userid = graphene.ID()
-    photo = graphene.Field(FieldFileType)
-    avatar = graphene.Field(FieldFileType)
+    photo = graphene.Field(FieldFileType, required=False)
+    avatar = graphene.Field(FieldFileType, required=False)
     avatar_url = graphene.String()
     avatar_content_url = graphene.String()
-    secondary_image_url = graphene.String()
+    secondary_image_url = graphene.String(required=False)
+
+    street = graphene.String(required=False)
+    housenumber = graphene.String(required=False)
+    postal_code = graphene.String(required=False)
+    place = graphene.String(required=False)
+
+    phone_number = graphene.String(required=False)
+    mobile_number = graphene.String(required=False)
+    email = graphene.String(required=False)
+
+    date_of_birth = graphene.String(required=False)
+    place_of_birth = graphene.String(required=False)
 
     notifications = graphene.List(NotificationType)
-    unread_notifications_count = graphene.Int()
+    unread_notifications_count = graphene.Int(required=False)
 
     is_dummy = graphene.Boolean()
     preferences = graphene.Field(PersonPreferencesType)
@@ -150,7 +162,11 @@ class PersonType(DjangoObjectType):
         return root.user.id if root.user else None
 
     def resolve_unread_notifications_count(root, info, **kwargs):  # noqa
-        return root.unread_notifications_count
+        if root.pk and has_person(info.context) and root == info.context.user.person:
+            return root.unread_notifications_count
+        elif root.pk:
+            return 0
+        return None
 
     def resolve_photo(root, info, **kwargs):
         if info.context.user.has_perm("core.view_photo_rule", root):
@@ -199,11 +215,11 @@ class PersonType(DjangoObjectType):
         return root.is_dummy if hasattr(root, "is_dummy") else False
 
     def resolve_notifications(root: Person, info, **kwargs):
-        if has_person(info.context.user) and info.context.user.person == root:
+        if root.pk and has_person(info.context) and root == info.context.user.person:
             return root.notifications.filter(send_at__lte=timezone.now()).order_by(
                 "read", "-created"
             )
-        raise PermissionDenied()
+        return []
 
     def resolve_can_edit_person(root, info, **kwargs):  # noqa
         return info.context.user.has_perm("core.edit_person_rule", root)
diff --git a/aleksis/core/schema/room.py b/aleksis/core/schema/room.py
index e1e478af053a3d46b42c7032a27700bb74b8289c..575130971671cfa79743b985918b3ad1621da53f 100644
--- a/aleksis/core/schema/room.py
+++ b/aleksis/core/schema/room.py
@@ -1,17 +1,21 @@
 from graphene_django import DjangoObjectType
-from graphene_django_cud.mutations import DjangoBatchDeleteMutation, DjangoBatchPatchMutation, DjangoCreateMutation
+from graphene_django_cud.mutations import (
+    DjangoBatchDeleteMutation,
+    DjangoBatchPatchMutation,
+    DjangoCreateMutation,
+)
 
 from ..models import Room
 from .base import (
     DeleteMutation,
     DjangoFilterMixin,
+    PermissionBatchDeleteMixin,
     PermissionBatchPatchMixin,
     PermissionsTypeMixin,
-    PermissionBatchDeleteMixin,
 )
 
 
-class RoomType(PermissionsTypeMixin, DjangoFilterMixin,  DjangoObjectType):
+class RoomType(PermissionsTypeMixin, DjangoFilterMixin, DjangoObjectType):
     class Meta:
         model = Room
         fields = ("id", "name", "short_name")
diff --git a/aleksis/core/schema/school_term.py b/aleksis/core/schema/school_term.py
index 092ae0e4b5e567210325ee5eca21f0e1b376d73f..798d1c62eb960d2c850a918345cbc03252a88820 100644
--- a/aleksis/core/schema/school_term.py
+++ b/aleksis/core/schema/school_term.py
@@ -1,8 +1,12 @@
-from django.core.exceptions import ValidationError, PermissionDenied
+from django.core.exceptions import PermissionDenied, ValidationError
 from django.utils.translation import gettext as _
 
 from graphene_django import DjangoObjectType
-from graphene_django_cud.mutations import DjangoBatchDeleteMutation, DjangoBatchPatchMutation, DjangoCreateMutation
+from graphene_django_cud.mutations import (
+    DjangoBatchDeleteMutation,
+    DjangoBatchPatchMutation,
+    DjangoCreateMutation,
+)
 
 from ..models import SchoolTerm
 from .base import (
diff --git a/docs/conf.py b/docs/conf.py
index 7d5706e4ab2a87ab9431f8ea7dcab000685cd0b4..919c2ec87fab03d7e7e6c7c982d695f76d559c43 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -25,13 +25,13 @@ django.setup()
 # -- Project information -----------------------------------------------------
 
 project = "AlekSIS-Core"
-copyright = "2019-2022 The AlekSIS team"
+copyright = "2019-2023 The AlekSIS team"
 author = "The AlekSIS Team"
 
 # The short X.Y version
 version = "3.1"
 # The full version, including alpha/beta/rc tags
-release = "3.1.1.dev0"
+release = "3.1.2.dev0"
 
 
 # -- General configuration ---------------------------------------------------
diff --git a/pyproject.toml b/pyproject.toml
index 31c7180aa202ebb38bcf984419fd6fe98e263520..468bf0a08826da58217865f095e3aee7b202d380 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
 [tool.poetry]
 name = "AlekSIS-Core"
-version = "3.1.1.dev0"
+version = "3.1.2.dev0"
 packages = [
     { include = "aleksis" }
 ]
@@ -136,6 +136,7 @@ sentry = ["sentry-sdk"]
 
 [tool.poetry.dev-dependencies]
 aleksis-builddeps = {version=">=2023.1.dev0", allow-prereleases=true}
+selenium = "<4.10.0"
 uwsgi = "^2.0"
 
 [tool.poetry.scripts]