diff --git a/aleksis/core/frontend/components/generic/InlineCRUDList.vue b/aleksis/core/frontend/components/generic/InlineCRUDList.vue
index d8ef84608fdc32c3dd08ddc696c14bd81bbf6eef..1a31de5fc285831d43eb713725b292473253d5a3 100644
--- a/aleksis/core/frontend/components/generic/InlineCRUDList.vue
+++ b/aleksis/core/frontend/components/generic/InlineCRUDList.vue
@@ -133,7 +133,7 @@
                         gqlCreateMutation: gqlCreateMutation,
                         gqlPatchMutation: gqlPatchMutation,
                         isCreate: true,
-                        fields: headers,
+                        fields: editableHeaders,
                         createItemI18nKey: createItemI18nKey,
                     }'
 
@@ -155,7 +155,7 @@
                     :gql-create-mutation="gqlCreateMutation"
                     :gql-patch-mutation="gqlPatchMutation"
                     :isCreate="true"
-                    :fields="headers"
+                    :fields="editableHeaders"
                     :create-item-i18n-key="createItemI18nKey"
                     @cancel="cancelCreate"
                     @save="handleCreateDone"
@@ -170,7 +170,7 @@
                     </template>
 
                     <template
-                      v-for="header in headers"
+                      v-for="header in editableHeaders"
                       #[formFieldSlotName(header)]="{ item, isCreate, on, attrs }"
                     >
                       <slot
@@ -204,7 +204,7 @@
           v-slot:[tableItemSlotName(header)]="{ item }"
         >
           <v-scroll-x-transition mode="out-in">
-            <span key="value" v-if="!editMode">
+            <span key="value" v-if="!editMode || header.disableEdit">
               <slot :name="header.value" :item="item">{{
                 item[header.value]
               }}</slot>
@@ -229,6 +229,10 @@
         </template>
 
         <template #item.actions="{ item }">
+          <slot
+            name="actions"
+            :item="item"
+          />
           <v-btn
             v-if="'canDelete' in item && item.canDelete"
             icon
@@ -334,6 +338,9 @@ export default {
         ] : [])
         .filter((header) => this.hiddenColumns.indexOf(header.value) === -1);
     },
+    editableHeaders() {
+      return this.headers.filter((header) => !header.disableEdit);
+    },
     elevationClass() {
       return this.elevated ? "elevation-2" : "";
     },
diff --git a/aleksis/core/frontend/components/generic/ObjectCRUDList.vue b/aleksis/core/frontend/components/generic/ObjectCRUDList.vue
index 40339e7b6d060f90214acf382c6ce7c75cad3c61..99fd458ab48a9516c5c4727083dba6438c87947e 100644
--- a/aleksis/core/frontend/components/generic/ObjectCRUDList.vue
+++ b/aleksis/core/frontend/components/generic/ObjectCRUDList.vue
@@ -5,103 +5,133 @@ import DialogObjectForm from "./dialogs/DialogObjectForm.vue";
 
 <template>
   <v-card>
-    <template v-if="!items && $apollo.queries.items.loading">
-      <slot name="loading">
-        <v-skeleton-loader type="card-heading, list-item-avatar-two-line@3, actions" />
-      </slot>
-    </template>
-    <template v-else>
-      <v-card-title>{{ title }}</v-card-title>
-      <v-list v-if="items.length">
-        <template v-for="(item, index) in items">
-          <v-list-item
-            :key="item.id"
-          >
-              <slot name="listItemContent" :item="item">
+    <v-data-iterator
+      :items="items"
+      :items-per-page="itemsPerPage"
+      :loading="$apollo.queries.items.loading"
+      hide-default-footer
+    >
+      <template #loading>
+        <slot name="loading">
+          <v-skeleton-loader type="card-heading, list-item-avatar-two-line@3, actions" />
+        </slot>
+      </template>
+
+      <template #no-data>
+        <v-card-text>{{ $t(noItemsI18nKey) }}</v-card-text>
+      </template>
+
+      <template #header>
+        <v-card-title>{{ title }}</v-card-title>
+      </template>
+
+      <template #default="props">
+        <slot
+          v-if="items.length"
+          name="iteratorContent"
+          :items="props.items"
+          :editing-enabled="editingEnabled"
+          :deletion-enabled="deletionEnabled"
+          :handle-edit="handleEdit"
+          :handle-delete="handleDelete"
+        >
+          <v-list>
+            <template v-for="(item, index) in items">
+              <v-list-item
+                :key="item.id"
+              >
+                <v-list-item-avatar>
+                  <slot name="listIteratorItemAvatar" :item="item" :index="index" />
+                </v-list-item-avatar>
                 <v-list-item-content>
-                  <v-list-item-title>
-                    {{ item.name }}
-                  </v-list-item-title>
+                  <slot name="listIteratorItemContent" :item="item" :index="index">
+                    <v-list-item-title>
+                      {{ item.name }}
+                    </v-list-item-title>
+                  </slot>
                 </v-list-item-content>
-              </slot>
-              <v-list-item-action v-if="editingEnabled && item.canEdit">
-                <slot name="listItemAction" :on="{
-                  input: handleEdit,
-                }">
-                  <v-btn icon @click="handleEdit(item)">
+                <v-list-item-action>
+                  <v-btn v-if="editingEnabled && item.canEdit" icon @click="handleEdit(item)">
                     <v-icon>mdi-pencil-outline</v-icon>
                   </v-btn>
-                </slot>
-              </v-list-item-action>
-          </v-list-item>
-          <v-divider
-            v-if="index < items.length - 1"
-            :key="index"
-            inset
-          ></v-divider>
-        </template>
-      </v-list>
-      <v-card-text v-else>{{ $t(noItemsI18nKey) }}</v-card-text>
-      <v-card-actions>
-        <slot
-          name="createComponent"
-          :attrs="{
-            value: objectFormModel,
-            defaultItem: defaultItem,
-            editItem: editItem,
-            gqlCreateMutation: gqlCreateMutation,
-            gqlPatchMutation: gqlPatchMutation,
-            isCreate: isCreate,
-            fields: fields,
-            getCreateData: getCreateData,
-            createItemI18nKey: createItemI18nKey,
-          }"
-          :on="{
-            input: (i) => (objectFormModel = i),
-            cancel: () => (objectFormModel = false),
-            save: handleCreateDone,
-            error: handleError,
-          }"
-        >
-          <dialog-object-form
-            v-model="objectFormModel"
-            :get-create-data="getCreateData"
-            :get-patch-data="getPatchData"
-            :default-item="defaultItem"
-            :edit-item="editItem"
-            :force-model-item-update="true"
-            :gql-create-mutation="gqlCreateMutation"
-            :gql-patch-mutation="gqlPatchMutation"
-            :is-create="isCreate"
-            :fields="fields"
-            :create-item-i18n-key="createItemI18nKey"
-            @cancel="objectFormModel = false"
-            @save="handleCreateDone"
-            @error="handleError"
-          >
-            <template #activator="{ props }">
-              <create-button
-                @click="handleCreate"
-                :disabled="objectFormModel"
-              />
+                  <v-btn v-if="deletionEnabled && item.canDelete" icon @click="handleDelete(item)">
+                    <v-icon>mdi-delete-outline</v-icon>
+                  </v-btn>
+                </v-list-item-action>
+              </v-list-item>
+              <v-divider
+                v-if="index < items.length - 1"
+                :key="index"
+                inset
+              ></v-divider>
             </template>
+          </v-list>
+        </slot>
+      </template>
 
-            <template
-              v-for="field in fields"
-              #[formFieldSlotName(field)]="{ item, isCreate, on, attrs }"
+      <template #footer>
+        <v-card-actions>
+          <slot
+            v-if="creatingEnabled || editingEnabled"
+            name="createComponent"
+            :attrs="{
+              value: objectFormModel,
+              defaultItem: defaultItem,
+              editItem: editItem,
+              gqlCreateMutation: gqlCreateMutation,
+              gqlPatchMutation: gqlPatchMutation,
+              isCreate: isCreate,
+              fields: fields,
+              getCreateData: getCreateData,
+              createItemI18nKey: createItemI18nKey,
+            }"
+            :on="{
+              input: (i) => (objectFormModel = i),
+              cancel: () => (objectFormModel = false),
+              save: handleCreateDone,
+              error: handleError,
+            }"
+          >
+            <dialog-object-form
+              v-model="objectFormModel"
+              :get-create-data="getCreateData"
+              :get-patch-data="getPatchData"
+              :default-item="defaultItem"
+              :edit-item="editItem"
+              :force-model-item-update="true"
+              :gql-create-mutation="gqlCreateMutation"
+              :gql-patch-mutation="gqlPatchMutation"
+              :is-create="isCreate"
+              :fields="fields"
+              :create-item-i18n-key="createItemI18nKey"
+              @cancel="objectFormModel = false"
+              @save="handleCreateDone"
+              @error="handleError"
             >
-              <slot
-                :name="formFieldSlotName(field)"
-                :attrs="attrs"
-                :on="on"
-                :item="item"
-                :isCreate="isCreate"
-              />
-            </template>
-          </dialog-object-form>
-        </slot>
-      </v-card-actions>
-    </template>
+              <template #activator="{ props }" v-if="creatingEnabled">
+                <create-button
+                  @click="handleCreate"
+                  :disabled="objectFormModel"
+                />
+              </template>
+
+              <template
+                v-for="field in fields"
+                #[formFieldSlotName(field)]="{ item, isCreate, on, attrs }"
+              >
+                <slot
+                  :name="formFieldSlotName(field)"
+                  :attrs="attrs"
+                  :on="on"
+                  :item="item"
+                  :isCreate="isCreate"
+                />
+              </template>
+            </dialog-object-form>
+          </slot>
+        </v-card-actions>
+      </template>
+    </v-data-iterator>
   </v-card>
 </template>
 
@@ -130,11 +160,13 @@ export default {
     },
     fields: {
       type: Array,
-      required: true,
+      required: false,
+      default: undefined,
     },
     defaultItem: {
       type: Object,
-      required: true,
+      required: false,
+      default: undefined,
     },
     getGqlData: {
       type: Function,
@@ -145,9 +177,15 @@ export default {
       type: Object,
       required: true,
     },
+    gqlVariables: {
+      type: Object,
+      required: false,
+      default: undefined,
+    },
     gqlCreateMutation: {
       type: Object,
-      required: true,
+      required: false,
+      default: undefined,
     },
     gqlPatchMutation: {
       type: Object,
@@ -172,6 +210,11 @@ export default {
         return patchItem;
       },
     },
+    itemsPerPage: {
+      type: Number,
+      required: false,
+      default: 5,
+    }
   },
   components: {
     CreateButton,
@@ -187,6 +230,12 @@ export default {
     items() {
       return {
         query: this.gqlQuery,
+        variables() {
+          if (this.gqlVariables) {
+            return this.gqlVariables;
+          }
+          return {};
+        },
         error: function (error) {
           this.handleError(error);
         },
@@ -210,6 +259,9 @@ export default {
       this.editItem = item;
       this.isCreate = false;
       this.objectFormModel = true;
+    },
+    handleDelete() {
+
     },
     handleCreateDone() {
       this.$apollo.queries.items.refetch();
@@ -237,9 +289,19 @@ export default {
     },
   },
   computed: {
+    creatingEnabled() {
+      return (
+        this.gqlCreateMutation &&
+        this.fields &&
+        this.defaultItem
+      );
+    },
     editingEnabled() {
       return (
-        this.gqlPatchMutation && this.items && this.items.some((i) => i.canEdit)
+        this.gqlPatchMutation &&
+        this.fields &&
+        this.items &&
+        this.items.some((i) => i.canEdit)
       );
     },
     deletionEnabled() {
diff --git a/aleksis/core/frontend/components/generic/buttons/BaseButton.vue b/aleksis/core/frontend/components/generic/buttons/BaseButton.vue
index 781525f54f221fedca51bbc74f383ee118c7d2a3..875b01b4105c1223b7011ad203af10f39adb2798 100644
--- a/aleksis/core/frontend/components/generic/buttons/BaseButton.vue
+++ b/aleksis/core/frontend/components/generic/buttons/BaseButton.vue
@@ -1,5 +1,5 @@
 <template>
-  <v-btn v-bind="{ ...$props, ...$attrs }" @click="$emit('click')">
+  <v-btn v-bind="{ ...$props, ...$attrs }" @click="$emit('click', $event)">
     <slot>
       <v-icon v-if="iconText" v-text="iconText" left/>
       <span v-t="i18nKey"/>
diff --git a/aleksis/core/frontend/components/generic/forms/DateTimeField.vue b/aleksis/core/frontend/components/generic/forms/DateTimeField.vue
new file mode 100644
index 0000000000000000000000000000000000000000..da046a69b36014968995db908d5190e5f027ca30
--- /dev/null
+++ b/aleksis/core/frontend/components/generic/forms/DateTimeField.vue
@@ -0,0 +1,115 @@
+<script setup>
+import DateField from "./DateField.vue";
+import TimeField from "./TimeField.vue";
+</script>
+
+<template>
+  <v-row>
+    <v-col cols="7">
+      <date-field
+        v-model="date"
+        v-bind="{ ...$attrs }"
+        :label="$t('forms.date_time.date')"
+        :min="minDate"
+        :max="maxDate"
+      />
+    </v-col>
+    <v-col cols="5">
+      <time-field
+        v-model="time"
+        v-bind="{ ...$attrs }"
+        :label="$t('forms.date_time.time')"
+        :min="minTime"
+        :max="maxTime"
+      />
+    </v-col>
+  </v-row>
+</template>
+
+<script>
+export default {
+  name: "DateTimeField",
+  data() {
+    return {
+      innerDateTime: this.value,
+    };
+  },
+  props: {
+    value: {
+      type: String,
+      required: false,
+      default: undefined,
+    },
+    minDate: {
+      type: String,
+      required: false,
+      default: undefined,
+    },
+    maxDate: {
+      type: String,
+      required: false,
+      default: undefined,
+    },
+    minTime: {
+      type: String,
+      required: false,
+      default: undefined,
+    },
+    maxTime: {
+      type: String,
+      required: false,
+      default: undefined,
+    },
+  },
+  computed: {
+    dateTime: {
+      get() {
+        return new Date(this.innerDateTime);
+      },
+      set(value) {
+        this.innerDateTime = value;
+        this.$emit("input", value);
+      },
+    },
+    date: {
+      get() {
+        return this.dateTime.toISOString().split("T")[0];
+      },
+      set(value) {
+        let newDateTime = this.dateTime;
+        const [year, month, day] = value.split("-");
+
+        newDateTime.setFullYear(year);
+        newDateTime.setMonth(month - 1);
+        newDateTime.setDate(day);
+
+        this.dateTime = newDateTime.toISOString();
+      },
+    },
+    time: {
+      get() {
+        return `${("0" + this.dateTime.getHours()).slice(-2)}:${(
+          "0" + this.dateTime.getMinutes()
+        ).slice(-2)}`;
+      },
+      set(value) {
+        let newDateTime = this.dateTime;
+
+        const [hours, minutes] = value.split(":");
+
+        newDateTime.setHours(hours);
+        newDateTime.setMinutes(minutes);
+
+        this.dateTime = newDateTime.toISOString();
+      },
+    },
+  },
+  watch: {
+    value(newValue) {
+      this.innerDateTime = newValue;
+    },
+  },
+};
+</script>
+
+<style scoped></style>
diff --git a/aleksis/core/frontend/components/generic/forms/TimeField.vue b/aleksis/core/frontend/components/generic/forms/TimeField.vue
index 7f5b0f03ca425a30c84962083c1b70f71d373825..e21014739d38fc66b8e4215a403b728dd7b5c52d 100644
--- a/aleksis/core/frontend/components/generic/forms/TimeField.vue
+++ b/aleksis/core/frontend/components/generic/forms/TimeField.vue
@@ -1,26 +1,114 @@
 <template>
-  <v-text-field
-    v-bind="$attrs"
-    v-on="$listeners"
-    placeholder="HH:MM[:SS]"
-    :rules="rules"
-  />
+  <v-menu
+    ref="menu"
+    v-model="menu"
+    :close-on-content-click="false"
+    transition="scale-transition"
+    offset-y
+    min-width="290"
+    eager
+  >
+    <template v-slot:activator="{ on, attrs }">
+      <v-text-field
+        v-model="time"
+        v-bind="{ ...$attrs, ...attrs }"
+        @click="handleClick"
+        @focusin="handleFocusIn"
+        @focusout="handleFocusOut"
+        @click:clear="handleClickClear"
+        placeholder="HH:MM[:SS]"
+        @keydown.esc="menu = false"
+        @keydown.enter="menu = false"
+        :prepend-icon="prependIcon"
+        :rules="rules"
+      ></v-text-field>
+    </template>
+    <v-time-picker
+      v-model="time"
+      ref="picker"
+      :min="min"
+      :max="max"
+      full-width
+      format="24hr"
+      @click:minute="menu = false"
+    ></v-time-picker>
+  </v-menu>
 </template>
 
 <script>
-
 export default {
   name: "TimeField",
+  extends: "v-text-field",
   data() {
     return {
+      menu: false,
+      innerTime: this.value,
+      openDueToFocus: true,
       rules: [
-          v => !v || /^([01]\d|2[0-3]):([0-5]\d)(:([0-5]\d))?$/.test(v) || this.$t("forms.errors.invalid_time"),
-      ]
-    }
+        (v) =>
+          !v ||
+          /^([01]\d|2[0-3]):([0-5]\d)(:([0-5]\d))?$/.test(v) ||
+          this.$t("forms.errors.invalid_time"),
+      ],
+    };
+  },
+  props: {
+    value: {
+      type: String,
+      required: false,
+      default: undefined,
+    },
+    min: {
+      type: String,
+      required: false,
+      default: undefined,
+    },
+    max: {
+      type: String,
+      required: false,
+      default: undefined,
+    },
+    prependIcon: {
+      type: String,
+      required: false,
+      default: undefined,
+    },
+  },
+  computed: {
+    time: {
+      get() {
+        return this.innerTime;
+      },
+      set(value) {
+        this.innerTime = value;
+        this.$emit("input", value);
+      },
+    },
   },
-}
+  methods: {
+    handleClickClear() {
+      if (this.clearable) {
+        this.time = null;
+      }
+    },
+    handleClick() {
+      this.menu = true;
+      this.openDueToFocus = false;
+    },
+    handleFocusIn() {
+      this.openDueToFocus = true;
+      this.menu = true;
+    },
+    handleFocusOut() {
+      if (this.openDueToFocus) this.menu = false;
+    },
+  },
+  watch: {
+    value(newValue) {
+      this.innerTime = newValue;
+    },
+  },
+};
 </script>
 
-<style scoped>
-
-</style>
+<style scoped></style>
diff --git a/aleksis/core/frontend/messages/en.json b/aleksis/core/frontend/messages/en.json
index 23ffc376f590f14b3aec8c17237926a34cb99d2c..439c9aeb2db072dbdbf8c4d213721efc98cfda78 100644
--- a/aleksis/core/frontend/messages/en.json
+++ b/aleksis/core/frontend/messages/en.json
@@ -297,6 +297,10 @@
       "not_a_whole_number": "Please enter a whole number",
       "number_too_small": "Please enter a bigger number.",
       "number_too_big": "Please enter a smaller number."
+    },
+    "date_time": {
+      "date": "Date",
+      "time": "Time"
     }
   },
   "weekdays": {
diff --git a/aleksis/core/schema/base.py b/aleksis/core/schema/base.py
index c928ed933e85d09657bb4f74a661d5d5bcc571df..8084bf594c62ac19ef759ff393c1a3a75342ca1c 100644
--- a/aleksis/core/schema/base.py
+++ b/aleksis/core/schema/base.py
@@ -82,9 +82,11 @@ class PermissionBatchPatchMixin:
         login_required = True
 
     @classmethod
-    def check_permissions(cls, root, info, input): # noqa
-        # TODO: Check PERMISSIONS (or rules)
-        return True
+    def check_permissions(cls, root, info, input):
+        if info.context.user.has_perms(cls._meta.permissions, root):
+            return
+
+        raise PermissionDenied()
 
 
 class PermissionBatchDeleteMixin:
@@ -92,9 +94,11 @@ class PermissionBatchDeleteMixin:
         login_required = True
 
     @classmethod
-    def check_permissions(cls, root, info, input): # noqa
-        # TODO: Check PERMISSIONS (or rules)
-        return True
+    def check_permissions(cls, root, info, input):
+        if info.context.user.has_perms(cls._meta.permissions, root):
+            return
+
+        raise PermissionDenied()
 
 
 class PermissionPatchMixin:
@@ -102,9 +106,11 @@ class PermissionPatchMixin:
         login_required = True
 
     @classmethod
-    def check_permissions(cls, root, info, input, id, obj): # noqa
-        # TODO: Check PERMISSIONS (or rules)
-        return True
+    def check_permissions(cls, root, info, input, id, obj):
+        if info.context.user.has_perms(cls._meta.permissions, root):
+            return
+
+        raise PermissionDenied()
 
 
 class DjangoFilterMixin: