diff --git a/.eslintrc.js b/.eslintrc.js
new file mode 100644
index 0000000000000000000000000000000000000000..996cd5aad4d7331e9c74dcaa7eb6483a5116eed9
--- /dev/null
+++ b/.eslintrc.js
@@ -0,0 +1,216 @@
+module.exports = {
+  extends: [
+    "eslint:recommended",
+    "plugin:vue/strongly-recommended",
+    //    "plugin:prettier/recommended",
+    "plugin:@intlify/vue-i18n/recommended",
+  ],
+  rules: {
+    "no-unused-vars": "warn",
+    "vue/no-unused-vars": "off",
+    "vue/multi-word-component-names": "off",
+    "@intlify/vue-i18n/key-format-style": [
+      "error",
+      "snake_case",
+      {
+        splitByDots: false,
+      },
+    ],
+    // "@intlify/vue-i18n/no-unused-keys": ["warn", {}],
+    "@intlify/vue-i18n/no-raw-text": [
+      "error",
+      {
+        ignoreNodes: ["v-icon"],
+        ignorePattern: "^[-–—·#:()\\[\\]&\\.\\s]+$",
+      },
+    ],
+    // Fixes for prettier (avoid eslint-config-prettier)
+    // The following rules can be used in some cases. See the README for more
+    // information. (These are marked with `0` instead of `"off"` so that a
+    // script can distinguish them.)
+    curly: 0,
+    "lines-around-comment": 0,
+    "max-len": 0,
+    "no-confusing-arrow": 0,
+    "no-mixed-operators": 0,
+    "no-tabs": 0,
+    "no-unexpected-multiline": 0,
+    quotes: 0,
+    "@typescript-eslint/quotes": 0,
+    "babel/quotes": 0,
+    "vue/html-self-closing": 0,
+    "vue/max-len": 0,
+
+    // The rest are rules that you never need to enable when using Prettier.
+    "array-bracket-newline": "off",
+    "array-bracket-spacing": "off",
+    "array-element-newline": "off",
+    "arrow-parens": "off",
+    "arrow-spacing": "off",
+    "block-spacing": "off",
+    "brace-style": "off",
+    "comma-dangle": "off",
+    "comma-spacing": "off",
+    "comma-style": "off",
+    "computed-property-spacing": "off",
+    "dot-location": "off",
+    "eol-last": "off",
+    "func-call-spacing": "off",
+    "function-call-argument-newline": "off",
+    "function-paren-newline": "off",
+    "generator-star": "off",
+    "generator-star-spacing": "off",
+    "implicit-arrow-linebreak": "off",
+    indent: "off",
+    "jsx-quotes": "off",
+    "key-spacing": "off",
+    "keyword-spacing": "off",
+    "linebreak-style": "off",
+    "multiline-ternary": "off",
+    "newline-per-chained-call": "off",
+    "new-parens": "off",
+    "no-arrow-condition": "off",
+    "no-comma-dangle": "off",
+    "no-extra-parens": "off",
+    "no-extra-semi": "off",
+    "no-floating-decimal": "off",
+    "no-mixed-spaces-and-tabs": "off",
+    "no-multi-spaces": "off",
+    "no-multiple-empty-lines": "off",
+    "no-reserved-keys": "off",
+    "no-space-before-semi": "off",
+    "no-trailing-spaces": "off",
+    "no-whitespace-before-property": "off",
+    "no-wrap-func": "off",
+    "nonblock-statement-body-position": "off",
+    "object-curly-newline": "off",
+    "object-curly-spacing": "off",
+    "object-property-newline": "off",
+    "one-var-declaration-per-line": "off",
+    "operator-linebreak": "off",
+    "padded-blocks": "off",
+    "quote-props": "off",
+    "rest-spread-spacing": "off",
+    semi: "off",
+    "semi-spacing": "off",
+    "semi-style": "off",
+    "space-after-function-name": "off",
+    "space-after-keywords": "off",
+    "space-before-blocks": "off",
+    "space-before-function-paren": "off",
+    "space-before-function-parentheses": "off",
+    "space-before-keywords": "off",
+    "space-in-brackets": "off",
+    "space-in-parens": "off",
+    "space-infix-ops": "off",
+    "space-return-throw-case": "off",
+    "space-unary-ops": "off",
+    "space-unary-word-ops": "off",
+    "switch-colon-spacing": "off",
+    "template-curly-spacing": "off",
+    "template-tag-spacing": "off",
+    "unicode-bom": "off",
+    "wrap-iife": "off",
+    "wrap-regex": "off",
+    "yield-star-spacing": "off",
+    "@babel/object-curly-spacing": "off",
+    "@babel/semi": "off",
+    "@typescript-eslint/brace-style": "off",
+    "@typescript-eslint/comma-dangle": "off",
+    "@typescript-eslint/comma-spacing": "off",
+    "@typescript-eslint/func-call-spacing": "off",
+    "@typescript-eslint/indent": "off",
+    "@typescript-eslint/keyword-spacing": "off",
+    "@typescript-eslint/member-delimiter-style": "off",
+    "@typescript-eslint/no-extra-parens": "off",
+    "@typescript-eslint/no-extra-semi": "off",
+    "@typescript-eslint/object-curly-spacing": "off",
+    "@typescript-eslint/semi": "off",
+    "@typescript-eslint/space-before-blocks": "off",
+    "@typescript-eslint/space-before-function-paren": "off",
+    "@typescript-eslint/space-infix-ops": "off",
+    "@typescript-eslint/type-annotation-spacing": "off",
+    "babel/object-curly-spacing": "off",
+    "babel/semi": "off",
+    "flowtype/boolean-style": "off",
+    "flowtype/delimiter-dangle": "off",
+    "flowtype/generic-spacing": "off",
+    "flowtype/object-type-curly-spacing": "off",
+    "flowtype/object-type-delimiter": "off",
+    "flowtype/quotes": "off",
+    "flowtype/semi": "off",
+    "flowtype/space-after-type-colon": "off",
+    "flowtype/space-before-generic-bracket": "off",
+    "flowtype/space-before-type-colon": "off",
+    "flowtype/union-intersection-spacing": "off",
+    "react/jsx-child-element-spacing": "off",
+    "react/jsx-closing-bracket-location": "off",
+    "react/jsx-closing-tag-location": "off",
+    "react/jsx-curly-newline": "off",
+    "react/jsx-curly-spacing": "off",
+    "react/jsx-equals-spacing": "off",
+    "react/jsx-first-prop-new-line": "off",
+    "react/jsx-indent": "off",
+    "react/jsx-indent-props": "off",
+    "react/jsx-max-props-per-line": "off",
+    "react/jsx-newline": "off",
+    "react/jsx-one-expression-per-line": "off",
+    "react/jsx-props-no-multi-spaces": "off",
+    "react/jsx-tag-spacing": "off",
+    "react/jsx-wrap-multilines": "off",
+    "standard/array-bracket-even-spacing": "off",
+    "standard/computed-property-even-spacing": "off",
+    "standard/object-curly-even-spacing": "off",
+    "unicorn/empty-brace-spaces": "off",
+    "unicorn/no-nested-ternary": "off",
+    "unicorn/number-literal-case": "off",
+    "vue/array-bracket-newline": "off",
+    "vue/array-bracket-spacing": "off",
+    "vue/arrow-spacing": "off",
+    "vue/block-spacing": "off",
+    "vue/block-tag-newline": "off",
+    "vue/brace-style": "off",
+    "vue/comma-dangle": "off",
+    "vue/comma-spacing": "off",
+    "vue/comma-style": "off",
+    "vue/dot-location": "off",
+    "vue/func-call-spacing": "off",
+    "vue/html-closing-bracket-newline": "off",
+    "vue/html-closing-bracket-spacing": "off",
+    "vue/html-end-tags": "off",
+    "vue/html-indent": "off",
+    "vue/html-quotes": "off",
+    "vue/key-spacing": "off",
+    "vue/keyword-spacing": "off",
+    "vue/max-attributes-per-line": "off",
+    "vue/multiline-html-element-content-newline": "off",
+    "vue/multiline-ternary": "off",
+    "vue/mustache-interpolation-spacing": "off",
+    "vue/no-extra-parens": "off",
+    "vue/no-multi-spaces": "off",
+    "vue/no-spaces-around-equal-signs-in-attribute": "off",
+    "vue/object-curly-newline": "off",
+    "vue/object-curly-spacing": "off",
+    "vue/object-property-newline": "off",
+    "vue/operator-linebreak": "off",
+    "vue/quote-props": "off",
+    "vue/script-indent": "off",
+    "vue/singleline-html-element-content-newline": "off",
+    "vue/space-in-parens": "off",
+    "vue/space-infix-ops": "off",
+    "vue/space-unary-ops": "off",
+    "vue/template-curly-spacing": "off",
+  },
+  settings: {
+    "vue-i18n": {
+      localeDir: "./aleksis/core/frontend/messages/*.{json}",
+      messageSyntaxVersion: "^8.0.0",
+    },
+  },
+  env: {
+    es2021: true,
+  },
+  parserOptions: {
+    ecmaVersion: "latest",
+  },
+};
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index f8bcd06c637e4de890c6a8b96312a05f102ca0f8..b2274c0f1da0fa3578ec8730511431f71bb085a9 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,17 +1,17 @@
 include:
-    - project: "AlekSIS/official/AlekSIS"
-      file: /ci/general.yml
-    - project: "AlekSIS/official/AlekSIS"
-      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"
-      file: /ci/build/dist.yml
-    - project: "AlekSIS/official/AlekSIS"
-      file: /ci/publish/pypi.yml
-    - project: "AlekSIS/official/AlekSIS"
-      file: /ci/docker/image.yml
+  - project: "AlekSIS/official/AlekSIS"
+    file: /ci/general.yml
+  - project: "AlekSIS/official/AlekSIS"
+    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"
+    file: /ci/build/dist.yml
+  - project: "AlekSIS/official/AlekSIS"
+    file: /ci/publish/pypi.yml
+  - project: "AlekSIS/official/AlekSIS"
+    file: /ci/docker/image.yml
diff --git a/.prettierignore b/.prettierignore
new file mode 100644
index 0000000000000000000000000000000000000000..38d141b743fd55678f50077c0617924475817095
--- /dev/null
+++ b/.prettierignore
@@ -0,0 +1,91 @@
+# Byte-compiled / optimized / DLL files
+*$py.class
+*.py[cod]
+__pycache__/
+
+# Distribution / packaging
+*.egg
+*.egg-info/
+.Python
+.eggs/
+.installed.cfg
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+
+# Installer logs
+pip-delete-this-directory.txt
+pip-log.txt
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+
+# pyenv
+.python-version
+
+# Environments
+.env
+.venv
+ENV/
+env/
+venv/
+
+# Editors
+*~
+DEADJOE
+\#*#
+
+# IntelliJ
+.idea
+.idea/
+
+# Database
+db.sqlite3
+
+# Sphinx
+docs/_build/
+
+# TeX
+*.aux
+
+# Generated files
+/node_modules/
+/static/
+/whoosh_index/
+poetry.lock
+
+.coverage
+.mypy_cache/
+.tox/
+htmlcov/
+maintenance_mode_state.txt
+media/
+package-lock.json
+yarn.lock
+
+# VSCode
+.vscode/
+.history/
+*.code-workspace
+
+/cache
+
+# Add HTML files to avoid problems with unsupported Django templates
+*.html
+
+# Do not check/reformat generated files
+aleksis/core/util/licenses.json
+.vite/
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 0fca141daca6c006186b994755c7047e3e5e74bc..909a4ea7ee838a66f6a7e834cda7baac9eaf1516 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -14,9 +14,42 @@ Added
 
 * Add feature to manage instructions and track whether groups have been instructed accordingly (e. g. for alarm plans).
 
+Fixed
+~~~~~
+* Migrating failed due to an incorrect field reference.
+
+`3.0`_ - 2023-05-15
+-------------------
+
+Fixed
+~~~~~
+* In some cases, pages showing the count of extra marks and lessons with custom excuse types of
+  persons threw an error.
+* The redirection to generated class register PDF printouts did not work.
+* Some columns in the table showing statistics for the members of a group were labled wrongly.
+* Absences with custom excuse types were not counted correctly.
+* Tabs on the week overview page were not displayed.
+
+`3.0b0`_ - 2023-02-28
+---------------------
+
+This version requires AlekSIS-Core 3.0. It is incompatible with any previous
+version.
+
+Removed
+~~~~~~~
+
+* Legacy menu integration for AlekSIS-Core pre-3.0
+
+Added
+~~~~~
+
+* Add SPA support for AlekSIS-Core 3.0
+
 Changed
 ~~~~~~~
 
+* [Dev] Rename the "late" field in the PersonalNote model to "tardiness".
 * Use new icon set inside of models and templates
 * Run full register printout generation in background 
 
@@ -25,6 +58,7 @@ Fixed
 
 * Extra marks and excused absences were counted multiple times in some class register views.
 * Substitution teachers couldn't see any persons in the person list of a substituted lesson.
+* Events were shown for days not being inside the timetable schema in full register printout.
 
 `2.1.1`_ - 2022-09-01
 ---------------------
@@ -292,3 +326,5 @@ Fixed
 .. _2.0.1: https://edugit.org/AlekSIS/Official/AlekSIS-App-Alsijil/-/tags/2.0.1
 .. _2.1: https://edugit.org/AlekSIS/Official/AlekSIS-App-Alsijil/-/tags/2.1
 .. _2.1.1: https://edugit.org/AlekSIS/Official/AlekSIS-App-Alsijil/-/tags/2.1.1
+.. _3.0b0: https://edugit.org/AlekSIS/Official/AlekSIS-App-Alsijil/-/tags/3.0b0
+.. _3.0: https://edugit.org/AlekSIS/Official/AlekSIS-App-Alsijil/-/tags/3.0
diff --git a/aleksis/apps/alsijil/actions.py b/aleksis/apps/alsijil/actions.py
index 9c307017ee629eec9f03cc170ed673af92b306b8..623a96b95ad28e319d16b4b2595f74352a03655d 100644
--- a/aleksis/apps/alsijil/actions.py
+++ b/aleksis/apps/alsijil/actions.py
@@ -41,7 +41,7 @@ def delete_personal_note(modeladmin, request, queryset):
         personal_note.reset_values()
         notes.append(personal_note)
     PersonalNote.objects.bulk_update(
-        notes, fields=["absent", "excused", "late", "excuse_type", "remarks"]
+        notes, fields=["absent", "excused", "tardiness", "excuse_type", "remarks"]
     )
 
 
diff --git a/aleksis/apps/alsijil/filters.py b/aleksis/apps/alsijil/filters.py
index 1e03f7ba399699055a7fda25b4d8680f3617b6f3..7337b94f41f445ffcc595f13d4f55637e9072d30 100644
--- a/aleksis/apps/alsijil/filters.py
+++ b/aleksis/apps/alsijil/filters.py
@@ -27,20 +27,20 @@ class PersonalNoteFilter(FilterSet):
                     data[name] = initial
 
         super().__init__(data, *args, **kwargs)
-        self.form.fields["late__lt"].label = _("Tardiness is lower than")
-        self.form.fields["late__gt"].label = _("Tardiness is bigger than")
+        self.form.fields["tardiness__lt"].label = _("Tardiness is lower than")
+        self.form.fields["tardiness__gt"].label = _("Tardiness is bigger than")
         self.form.layout = Layout(
             Row("subject"),
             Row("day_start", "day_end"),
             Row("absent", "excused", "excuse_type"),
-            Row("late__gt", "late__lt", "extra_marks"),
+            Row("tardiness__gt", "tardiness__lt", "extra_marks"),
         )
 
     class Meta:
         model = PersonalNote
         fields = {
             "excused": ["exact"],
-            "late": ["lt", "gt"],
+            "tardiness": ["lt", "gt"],
             "absent": ["exact"],
             "excuse_type": ["exact"],
             "extra_marks": ["exact"],
diff --git a/aleksis/apps/alsijil/forms.py b/aleksis/apps/alsijil/forms.py
index d4353f381308cb079440c16d2362fb2e3f095cf3..8c6d0b5147fd4385639aac15464ed323b90f72f1 100644
--- a/aleksis/apps/alsijil/forms.py
+++ b/aleksis/apps/alsijil/forms.py
@@ -78,7 +78,7 @@ class LessonDocumentationForm(forms.ModelForm):
 class PersonalNoteForm(forms.ModelForm):
     class Meta:
         model = PersonalNote
-        fields = ["absent", "late", "excused", "excuse_type", "extra_marks", "remarks"]
+        fields = ["absent", "tardiness", "excused", "excuse_type", "extra_marks", "remarks"]
 
     person_name = forms.CharField(disabled=True)
 
diff --git a/aleksis/apps/alsijil/frontend/index.js b/aleksis/apps/alsijil/frontend/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..38293a43aeee9f3b9dc25a725f2531ebae71388c
--- /dev/null
+++ b/aleksis/apps/alsijil/frontend/index.js
@@ -0,0 +1,407 @@
+import {
+  notLoggedInValidator,
+  hasPersonValidator,
+} from "aleksis.core/routeValidators";
+
+export default {
+  meta: {
+    inMenu: true,
+    titleKey: "alsijil.menu_title",
+    icon: "mdi-account-group-outline",
+    validators: [hasPersonValidator],
+  },
+  props: {
+    byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+  },
+  children: [
+    {
+      path: "lesson",
+      component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+      name: "alsijil.lessonPeriod",
+      meta: {
+        inMenu: true,
+        titleKey: "alsijil.lesson.menu_title",
+        icon: "mdi-alarm",
+        permission: "alsijil.view_lesson_menu_rule",
+      },
+      props: {
+        byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+      },
+    },
+    {
+      path: "lesson/:year(\\d+)/:week(\\d+)/:id_(\\d+)",
+      component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+      name: "alsijil.lessonPeriodByCWAndID",
+      props: {
+        byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+      },
+    },
+    {
+      path: "extra_lesson/:id_(\\d+)/",
+      component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+      name: "alsijil.extraLessonByID",
+      props: {
+        byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+      },
+    },
+    {
+      path: "event/:id_(\\d+)/",
+      component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+      name: "alsijil.eventByID",
+      props: {
+        byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+      },
+    },
+    {
+      path: "week/",
+      component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+      name: "alsijil.weekView",
+      meta: {
+        inMenu: true,
+        titleKey: "alsijil.week.menu_title",
+        icon: "mdi-view-week-outline",
+        permission: "alsijil.view_week_menu_rule",
+      },
+      props: {
+        byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+      },
+    },
+    {
+      path: "week/:year(\\d+)/:week(\\d+)/",
+      component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+      name: "alsijil.weekViewByWeek",
+      props: {
+        byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+      },
+    },
+    {
+      path: "week/year/cw/",
+      component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+      name: "alsijil.weekViewPlaceholders",
+      props: {
+        byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+      },
+    },
+    {
+      path: "week/:type_/:id_(\\d+)/",
+      component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+      name: "alsijil.weekViewByTypeAndID",
+      props: {
+        byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+      },
+    },
+    {
+      path: "week/year/cw/:type_/:id_(\\d+)/",
+      component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+      name: "alsijil.weekViewPlaceholdersByTypeAndID",
+      props: {
+        byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+      },
+    },
+    {
+      path: "week/:year(\\d+)/:week(\\d+)/:type_/:id_(\\d+)/",
+      component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+      name: "alsijil.weekViewByWeekTypeAndID",
+      props: {
+        byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+      },
+    },
+    {
+      path: "print/group/:id_(\\d+)",
+      component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+      name: "alsijil.fullRegisterGroup",
+      props: {
+        byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+      },
+    },
+    {
+      path: "groups/",
+      component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+      name: "alsijil.myGroups",
+      meta: {
+        inMenu: true,
+        titleKey: "alsijil.groups.menu_title",
+        icon: "mdi-account-multiple-outline",
+        permission: "alsijil.view_my_groups_rule",
+      },
+      props: {
+        byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+      },
+    },
+    {
+      path: "groups/:pk(\\d+)/",
+      component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+      name: "alsijil.studentsList",
+      props: {
+        byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+      },
+    },
+    {
+      path: "persons/",
+      component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+      name: "alsijil.myStudents",
+      meta: {
+        inMenu: true,
+        titleKey: "alsijil.persons.menu_title",
+        icon: "mdi-account-school-outline",
+        permission: "alsijil.view_my_students_rule",
+      },
+      props: {
+        byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+      },
+    },
+    {
+      path: "instructions/",
+      component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+      name: "alsijil.instructions",
+      meta: {
+        inMenu: true,
+        titleKey: "alsijil.instruction.menu_title",
+        icon: "mdi-folder-check-outline",
+        permission: "alsijil.view_instructions_rule"
+      }
+  },
+    {
+      path: "persons/:id_(\\d+)/",
+      component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+      name: "alsijil.overviewPerson",
+      props: {
+        byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+      },
+    },
+    {
+      path: "me/",
+      component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+      name: "alsijil.overviewMe",
+      meta: {
+        inMenu: true,
+        titleKey: "alsijil.my_overview.menu_title",
+        icon: "mdi-chart-box-outline",
+        permission: "alsijil.view_person_overview_menu_rule",
+      },
+      props: {
+        byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+      },
+    },
+    {
+      path: "notes/:pk(\\d+)/delete/",
+      component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+      name: "alsijil.deletePersonalNote",
+      props: {
+        byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+      },
+    },
+    {
+      path: "absence/new/:id_(\\d+)/",
+      component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+      name: "alsijil.registerAbsenceWithID",
+      props: {
+        byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+      },
+    },
+    {
+      path: "absence/new/",
+      component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+      name: "alsijil.registerAbsence",
+      meta: {
+        inMenu: true,
+        titleKey: "alsijil.absence.menu_title",
+        icon: "mdi-message-alert-outline",
+        permission: "alsijil.view_register_absence_rule",
+      },
+      props: {
+        byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+      },
+    },
+    {
+      path: "extra_marks/",
+      component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+      name: "alsijil.extraMarks",
+      meta: {
+        inMenu: true,
+        titleKey: "alsijil.extra_marks.menu_title",
+        icon: "mdi-label-variant-outline",
+        permission: "alsijil.view_extramarks_rule",
+      },
+      props: {
+        byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+      },
+    },
+    {
+      path: "extra_marks/create/",
+      component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+      name: "alsijil.createExtraMark",
+      props: {
+        byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+      },
+    },
+    {
+      path: "extra_marks/:pk(\\d+)/edit/",
+      component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+      name: "alsijil.editExtraMark",
+      props: {
+        byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+      },
+    },
+    {
+      path: "extra_marks/:pk(\\d+)/delete/",
+      component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+      name: "alsijil.deleteExtraMark",
+      props: {
+        byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+      },
+    },
+    {
+      path: "excuse_types/",
+      component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+      name: "alsijil.excuseTypes",
+      meta: {
+        inMenu: true,
+        titleKey: "alsijil.excuse_types.menu_title",
+        icon: "mdi-label-outline",
+        permission: "alsijil.view_excusetypes_rule",
+      },
+      props: {
+        byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+      },
+    },
+    {
+      path: "excuse_types/create/",
+      component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+      name: "alsijil.createExcuseType",
+      props: {
+        byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+      },
+    },
+    {
+      path: "excuse_types/:pk(\\d+)/edit/",
+      component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+      name: "alsijil.editExcuseType",
+      props: {
+        byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+      },
+    },
+    {
+      path: "excuse_types/:pk(\\d+)/delete/",
+      component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+      name: "alsijil.deleteExcuseType",
+      props: {
+        byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+      },
+    },
+    {
+      path: "group_roles/",
+      component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+      name: "alsijil.groupRoles",
+      meta: {
+        inMenu: true,
+        titleKey: "alsijil.group_roles.menu_title_manage",
+        icon: "mdi-clipboard-plus-outline",
+        permission: "alsijil.view_grouproles_rule",
+      },
+      props: {
+        byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+      },
+    },
+    {
+      path: "group_roles/create/",
+      component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+      name: "alsijil.createGroupRole",
+      props: {
+        byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+      },
+    },
+    {
+      path: "group_roles/:pk(\\d+)/edit/",
+      component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+      name: "alsijil.editGroupRole",
+      props: {
+        byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+      },
+    },
+    {
+      path: "group_roles/:pk(\\d+)/delete/",
+      component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+      name: "alsijil.deleteGroupRole",
+      props: {
+        byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+      },
+    },
+    {
+      path: "groups/:pk(\\d+)/group_roles/",
+      component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+      name: "alsijil.assignedGroupRoles",
+      props: {
+        byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+      },
+    },
+    {
+      path: "groups/:pk(\\d+)/group_roles/assign/",
+      component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+      name: "alsijil.assignGroupRole",
+      props: {
+        byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+      },
+    },
+    {
+      path: "groups/:pk(\\d+)/group_roles/:role_pk(\\d+)/assign/",
+      component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+      name: "alsijil.assignGroupRoleByRolePK",
+      props: {
+        byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+      },
+    },
+    {
+      path: "group_roles/assignments/:pk(\\d+)/edit/",
+      component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+      name: "alsijil.editGroupRoleAssignment",
+      props: {
+        byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+      },
+    },
+    {
+      path: "group_roles/assignments/:pk(\\d+)/stop/",
+      component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+      name: "alsijil.stopGroupRoleAssignment",
+      props: {
+        byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+      },
+    },
+    {
+      path: "group_roles/assignments/:pk(\\d+)/delete/",
+      component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+      name: "alsijil.deleteGroupRoleAssignment",
+      props: {
+        byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+      },
+    },
+    {
+      path: "group_roles/assignments/assign/",
+      component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+      name: "alsijil.assignGroupRoleMultiple",
+      meta: {
+        inMenu: true,
+        titleKey: "alsijil.group_roles.menu_title_assign",
+        icon: "mdi-clipboard-account-outline",
+        permission: "alsijil.assign_grouprole_for_multiple_rule",
+      },
+      props: {
+        byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+      },
+    },
+    {
+      path: "all/",
+      component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+      name: "alsijil.allRegisterObjects",
+      meta: {
+        inMenu: true,
+        titleKey: "alsijil.all_lessons.menu_title",
+        icon: "mdi-format-list-text",
+        permission: "alsijil.view_register_objects_list_rule",
+      },
+      props: {
+        byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+      },
+    },
+  ],
+};
diff --git a/aleksis/apps/alsijil/frontend/messages/de.json b/aleksis/apps/alsijil/frontend/messages/de.json
new file mode 100644
index 0000000000000000000000000000000000000000..09456c74481a13e390b1e08e2389d027d8c6974d
--- /dev/null
+++ b/aleksis/apps/alsijil/frontend/messages/de.json
@@ -0,0 +1,36 @@
+{
+  "alsijil": {
+    "menu_title": "Klassenbuch",
+    "lesson": {
+      "menu_title": "Aktuelle Unterrichtsstunde"
+    },
+    "week": {
+      "menu_title": "Aktuelle Woche"
+    },
+    "groups": {
+      "menu_title": "Meine Gruppen"
+    },
+    "persons": {
+      "menu_title": "Meine Schülerinnen und Schüler"
+    },
+    "absence": {
+      "menu_title": "Abwesenheit eintragen"
+    },
+    "my_overview": {
+      "menu_title": "Meine Übersicht"
+    },
+    "excuse_types": {
+      "menu_title": "Entschuldigungsarten"
+    },
+    "group_roles": {
+      "menu_title_assign": "Gruppenrollen zuweisen",
+      "menu_title_manage": "Gruppenrollen verwalten"
+    },
+    "extra_marks": {
+      "menu_title": "Zusätzliche Markierungen"
+    },
+    "all_lessons": {
+      "menu_title": "Alle Stunden"
+    }
+  }
+}
diff --git a/aleksis/apps/alsijil/frontend/messages/en.json b/aleksis/apps/alsijil/frontend/messages/en.json
new file mode 100644
index 0000000000000000000000000000000000000000..cd9798229b0d867611da8dc6b89dfe02eae91f85
--- /dev/null
+++ b/aleksis/apps/alsijil/frontend/messages/en.json
@@ -0,0 +1,36 @@
+{
+  "alsijil": {
+    "lesson": {
+      "menu_title": "Current lesson"
+    },
+    "week": {
+      "menu_title": "Current week"
+    },
+    "groups": {
+      "menu_title": "My groups"
+    },
+    "persons": {
+      "menu_title": "My students"
+    },
+    "absence": {
+      "menu_title": "Register absence"
+    },
+    "my_overview": {
+      "menu_title": "My overview"
+    },
+    "extra_marks": {
+      "menu_title": "Extra marks"
+    },
+    "excuse_types": {
+      "menu_title": "Excuse types"
+    },
+    "group_roles": {
+      "menu_title_manage": "Manage group roles",
+      "menu_title_assign": "Assign group roles"
+    },
+    "all_lessons": {
+      "menu_title": "All lessons"
+    },
+    "menu_title": "Class register"
+  }
+}
diff --git a/aleksis/apps/alsijil/frontend/messages/ru.json b/aleksis/apps/alsijil/frontend/messages/ru.json
new file mode 100644
index 0000000000000000000000000000000000000000..8fb7aea426577ea2406c1579dbe44b39b70e5ef6
--- /dev/null
+++ b/aleksis/apps/alsijil/frontend/messages/ru.json
@@ -0,0 +1,36 @@
+{
+  "alsijil": {
+    "my_overview": {
+      "menu_title": "Мой обзор"
+    },
+    "group_roles": {
+      "menu_title_manage": "Управление ролями групп",
+      "menu_title_assign": "Назначить роль группы"
+    },
+    "all_lessons": {
+      "menu_title": "Все уроки"
+    },
+    "menu_title": "Классный журнал",
+    "lesson": {
+      "menu_title": "Текущий урок"
+    },
+    "week": {
+      "menu_title": "Текущая неделя"
+    },
+    "groups": {
+      "menu_title": "Мои группы"
+    },
+    "persons": {
+      "menu_title": "Мои студенты"
+    },
+    "absence": {
+      "menu_title": "Регистрация отсутствия"
+    },
+    "extra_marks": {
+      "menu_title": "Дополнительные отметки"
+    },
+    "excuse_types": {
+      "menu_title": "Типы объяснительных"
+    }
+  }
+}
diff --git a/aleksis/apps/alsijil/frontend/messages/uk.json b/aleksis/apps/alsijil/frontend/messages/uk.json
new file mode 100644
index 0000000000000000000000000000000000000000..a29573bea059ddf91b6ae156be5d76914dd2ca50
--- /dev/null
+++ b/aleksis/apps/alsijil/frontend/messages/uk.json
@@ -0,0 +1,36 @@
+{
+  "alsijil": {
+    "week": {
+      "menu_title": "Поточний тиждень"
+    },
+    "groups": {
+      "menu_title": "Мої групи"
+    },
+    "persons": {
+      "menu_title": "Мої студенти"
+    },
+    "absence": {
+      "menu_title": "Реєстрація відсутності"
+    },
+    "my_overview": {
+      "menu_title": "Мій огляд"
+    },
+    "extra_marks": {
+      "menu_title": "Додаткові відмітки"
+    },
+    "excuse_types": {
+      "menu_title": "Типи пояснень"
+    },
+    "group_roles": {
+      "menu_title_manage": "Керування ролями групи",
+      "menu_title_assign": "Призначити роль групи"
+    },
+    "all_lessons": {
+      "menu_title": "Усі уроки"
+    },
+    "menu_title": "Класний журнал",
+    "lesson": {
+      "menu_title": "Поточний урок"
+    }
+  }
+}
diff --git a/aleksis/apps/alsijil/locale/ru/LC_MESSAGES/django.po b/aleksis/apps/alsijil/locale/ru/LC_MESSAGES/django.po
index 3619b388cb11b49e194af013e385a0b6d67b3ef4..fde32a7d86c67b9f2344ca1a4aa59435fcf86cfa 100644
--- a/aleksis/apps/alsijil/locale/ru/LC_MESSAGES/django.po
+++ b/aleksis/apps/alsijil/locale/ru/LC_MESSAGES/django.po
@@ -7,8 +7,8 @@ msgid ""
 msgstr ""
 "Project-Id-Version: PACKAGE VERSION\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2022-08-15 10:22+0200\n"
-"PO-Revision-Date: 2022-07-03 02:56+0000\n"
+"POT-Creation-Date: 2022-06-25 15:28+0200\n"
+"PO-Revision-Date: 2023-05-26 04:38+0000\n"
 "Last-Translator: Serhii Horichenko <m@sgg.im>\n"
 "Language-Team: Russian <https://translate.edugit.org/projects/aleksis/aleksis-app-alsijil/ru/>\n"
 "Language: ru\n"
@@ -355,7 +355,7 @@ msgstr "Короткое имя"
 #: aleksis/apps/alsijil/templates/alsijil/partials/persons_with_stats.html:12
 #: aleksis/apps/alsijil/templates/alsijil/partials/persons_with_stats.html:23
 msgid "Name"
-msgstr "Имя"
+msgstr "Полное имя"
 
 #: aleksis/apps/alsijil/models.py:60 aleksis/apps/alsijil/tables.py:41
 msgid "Count as absent"
diff --git a/aleksis/apps/alsijil/locale/uk/LC_MESSAGES/django.po b/aleksis/apps/alsijil/locale/uk/LC_MESSAGES/django.po
index f1bfeaa57fbc10f7104dac8e4e079330a234fe73..ff5ccddf60a3017b9db6076adc9a0c2a9cf16dc4 100644
--- a/aleksis/apps/alsijil/locale/uk/LC_MESSAGES/django.po
+++ b/aleksis/apps/alsijil/locale/uk/LC_MESSAGES/django.po
@@ -7,8 +7,8 @@ msgid ""
 msgstr ""
 "Project-Id-Version: PACKAGE VERSION\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2022-08-15 10:22+0200\n"
-"PO-Revision-Date: 2022-07-03 02:56+0000\n"
+"POT-Creation-Date: 2022-06-25 15:28+0200\n"
+"PO-Revision-Date: 2023-01-25 05:58+0000\n"
 "Last-Translator: Serhii Horichenko <m@sgg.im>\n"
 "Language-Team: Ukrainian <https://translate.edugit.org/projects/aleksis/aleksis-app-alsijil/uk/>\n"
 "Language: uk\n"
@@ -429,7 +429,7 @@ msgstr "Додаткова відмітка"
 
 #: aleksis/apps/alsijil/models.py:469 aleksis/apps/alsijil/models.py:530
 msgid "Icon"
-msgstr "Іконка"
+msgstr "Піктограма"
 
 #: aleksis/apps/alsijil/models.py:470
 msgid "Colour"
diff --git a/aleksis/apps/alsijil/managers.py b/aleksis/apps/alsijil/managers.py
index 9d97e58091af8ab2149122d7e51f767faccb61f6..c9e26fa77a83dfe76212478d58e36c08dca4f4a9 100644
--- a/aleksis/apps/alsijil/managers.py
+++ b/aleksis/apps/alsijil/managers.py
@@ -119,7 +119,7 @@ class PersonalNoteQuerySet(RegisterObjectRelatedQuerySet, QuerySet):
     def not_empty(self):
         """Get all not empty personal notes."""
         return self.filter(
-            ~Q(remarks="") | Q(absent=True) | ~Q(late=0) | Q(extra_marks__isnull=False)
+            ~Q(remarks="") | Q(absent=True) | ~Q(tardiness=0) | Q(extra_marks__isnull=False)
         )
 
 
diff --git a/aleksis/apps/alsijil/menus.py b/aleksis/apps/alsijil/menus.py
deleted file mode 100644
index 2edf685e21345b9f2dbf1faefa804053c40ec117..0000000000000000000000000000000000000000
--- a/aleksis/apps/alsijil/menus.py
+++ /dev/null
@@ -1,150 +0,0 @@
-from django.utils.translation import gettext_lazy as _
-
-MENUS = {
-    "NAV_MENU_CORE": [
-        {
-            "name": _("Class register"),
-            "url": "#",
-            "svg_icon": "mdi:book-open-outline",
-            "root": True,
-            "validators": [
-                "menu_generator.validators.is_authenticated",
-                "aleksis.core.util.core_helpers.has_person",
-            ],
-            "submenu": [
-                {
-                    "name": _("Current lesson"),
-                    "url": "lesson_period",
-                    "svg_icon": "mdi:alarm",
-                    "validators": [
-                        (
-                            "aleksis.core.util.predicates.permission_validator",
-                            "alsijil.view_lesson_menu_rule",
-                        ),
-                    ],
-                },
-                {
-                    "name": _("Current week"),
-                    "url": "week_view",
-                    "svg_icon": "mdi:view-week-outline",
-                    "validators": [
-                        (
-                            "aleksis.core.util.predicates.permission_validator",
-                            "alsijil.view_week_menu_rule",
-                        ),
-                    ],
-                },
-                {
-                    "name": _("My groups"),
-                    "url": "my_groups",
-                    "svg_icon": "mdi:account-multiple-outline",
-                    "validators": [
-                        (
-                            "aleksis.core.util.predicates.permission_validator",
-                            "alsijil.view_my_groups_rule",
-                        ),
-                    ],
-                },
-                {
-                    "name": _("My overview"),
-                    "url": "overview_me",
-                    "svg_icon": "mdi:chart-box-outline",
-                    "validators": [
-                        (
-                            "aleksis.core.util.predicates.permission_validator",
-                            "alsijil.view_person_overview_menu_rule",
-                        ),
-                    ],
-                },
-                {
-                    "name": _("My students"),
-                    "url": "my_students",
-                    "svg_icon": "mdi:account-school-outline",
-                    "validators": [
-                        (
-                            "aleksis.core.util.predicates.permission_validator",
-                            "alsijil.view_my_students_rule",
-                        ),
-                    ],
-                },
-                {
-                    "name": _("Instructions"),
-                    "url": "instructions",
-                    "icon": "rule_folder",
-                    "validators": [
-                        (
-                            "aleksis.core.util.predicates.permission_validator",
-                            "alsijil.view_instructions_rule",
-                        ),
-                    ],
-                },
-                {
-                    "name": _("Assign group role"),
-                    "url": "assign_group_role_multiple",
-                    "svg_icon": "mdi:clipboard-account-outline",
-                    "validators": [
-                        (
-                            "aleksis.core.util.predicates.permission_validator",
-                            "alsijil.assign_grouprole_for_multiple_rule",
-                        ),
-                    ],
-                },
-                {
-                    "name": _("All lessons"),
-                    "url": "all_register_objects",
-                    "svg_icon": "mdi:format-list-text",
-                    "validators": [
-                        (
-                            "aleksis.core.util.predicates.permission_validator",
-                            "alsijil.view_register_objects_list_rule",
-                        ),
-                    ],
-                },
-                {
-                    "name": _("Register absence"),
-                    "url": "register_absence",
-                    "icon": "rate_review",
-                    "validators": [
-                        (
-                            "aleksis.core.util.predicates.permission_validator",
-                            "alsijil.view_register_absence_rule",
-                        ),
-                    ],
-                },
-                {
-                    "name": _("Excuse types"),
-                    "url": "excuse_types",
-                    "svg_icon": "mdi:label-outline",
-                    "validators": [
-                        (
-                            "aleksis.core.util.predicates.permission_validator",
-                            "alsijil.view_excusetypes_rule",
-                        ),
-                    ],
-                },
-                {
-                    "name": _("Extra marks"),
-                    "url": "extra_marks",
-                    "svg_icon": "mdi:label-variant-outline",
-                    "validators": [
-                        (
-                            "aleksis.core.util.predicates.permission_validator",
-                            "alsijil.view_extramarks_rule",
-                        ),
-                    ],
-                },
-                {
-                    "name": _("Manage group roles"),
-                    "url": "group_roles",
-                    "svg_icon": "mdi:clipboard-plus-outline",
-                    "validators": [
-                        (
-                            "aleksis.core.util.predicates.permission_validator",
-                            "alsijil.view_grouproles_rule",
-                        ),
-                    ],
-                },
-            ],
-        }
-    ]
-}
diff --git a/aleksis/apps/alsijil/migrations/0001_initial.py b/aleksis/apps/alsijil/migrations/0001_initial.py
index 4a88b98b89eb6b5ed6aa914096c032b689710bde..e49edfac8a4cdb1f2684c3f2667d3e1ef18de003 100644
--- a/aleksis/apps/alsijil/migrations/0001_initial.py
+++ b/aleksis/apps/alsijil/migrations/0001_initial.py
@@ -130,7 +130,7 @@ class Migration(migrations.Migration):
                 "verbose_name": "Personal note",
                 "verbose_name_plural": "Personal notes",
                 "ordering": [
-                    "lesson_period__lesson__date_start",
+                    "lesson_period__lesson__validity__date_start",
                     "week",
                     "lesson_period__period__weekday",
                     "lesson_period__period__period",
@@ -194,7 +194,7 @@ class Migration(migrations.Migration):
                 "verbose_name": "Lesson documentation",
                 "verbose_name_plural": "Lesson documentations",
                 "ordering": [
-                    "lesson_period__lesson__date_start",
+                    "lesson_period__lesson__validity__date_start",
                     "week",
                     "lesson_period__period__weekday",
                     "lesson_period__period__period",
diff --git a/aleksis/apps/alsijil/migrations/0017_rename_late_to_tardiness.py b/aleksis/apps/alsijil/migrations/0017_rename_late_to_tardiness.py
new file mode 100644
index 0000000000000000000000000000000000000000..50dd5a18d2ce0271a3a145ff1f3605aeb1b6813e
--- /dev/null
+++ b/aleksis/apps/alsijil/migrations/0017_rename_late_to_tardiness.py
@@ -0,0 +1,18 @@
+# Generated by Django 3.2.15 on 2022-08-27 15:02
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('alsijil', '0016_add_not_counted_excuse_types'),
+    ]
+
+    operations = [
+        migrations.RenameField(
+            model_name='personalnote',
+            old_name='late',
+            new_name='tardiness',
+        ),
+    ]
diff --git a/aleksis/apps/alsijil/model_extensions.py b/aleksis/apps/alsijil/model_extensions.py
index 2beb3c113c13556d99f6541a6bcb34c9cd0baaed..abbedd709dd3604172b831c2c931a2ea5f198b5a 100644
--- a/aleksis/apps/alsijil/model_extensions.py
+++ b/aleksis/apps/alsijil/model_extensions.py
@@ -52,7 +52,7 @@ def mark_absent(
     to_period: Optional[int] = None,
     dry_run: bool = False,
 ):
-    """Mark a person absent for all lessons in a day, optionally starting with a selected period number.
+    """Mark a person absent for all lessons in a day, optionally starting with a period number.
 
     This function creates `PersonalNote` objects for every `LessonPeriod` and `ExtraLesson`
     the person participates in on the selected day and marks them as absent/excused.
@@ -326,12 +326,12 @@ def get_tardinesses(self, week: Optional[CalendarWeek] = None) -> QuerySet:
     """Get all personal notes of late persons for this lesson."""
     if not week:
         week = self.week
-    return self.personal_notes.filter(week=week.week, year=week.year, late__gt=0)
+    return self.personal_notes.filter(week=week.week, year=week.year, tardiness__gt=0)
 
 
 def get_tardinesses_simple(self, week: Optional[CalendarWeek] = None) -> QuerySet:
     """Get all personal notes of late persons for this event/extra lesson."""
-    return self.personal_notes.filter(late__gt=0)
+    return self.personal_notes.filter(tardiness__gt=0)
 
 
 Event.method(get_tardinesses_simple, "get_tardinesses")
@@ -459,9 +459,11 @@ def generate_person_list_with_class_register_statistics(
             filter=Q(filtered_personal_notes__absent=True, filtered_personal_notes__excused=False),
             distinct=True,
         ),
-        tardiness=Sum("filtered_personal_notes__late"),
+        tardiness=Sum("filtered_personal_notes__tardiness"),
         tardiness_count=Count(
-            "filtered_personal_notes", filter=Q(filtered_personal_notes__late__gt=0), distinct=True
+            "filtered_personal_notes",
+            filter=Q(filtered_personal_notes__tardiness__gt=0),
+            distinct=True,
         ),
     )
 
@@ -480,7 +482,7 @@ def generate_person_list_with_class_register_statistics(
         persons = persons.annotate(
             **{
                 excuse_type.count_label: Count(
-                    "filtered_personal_notes__absent",
+                    "filtered_personal_notes",
                     filter=Q(
                         filtered_personal_notes__absent=True,
                         filtered_personal_notes__excuse_type=excuse_type,
diff --git a/aleksis/apps/alsijil/models.py b/aleksis/apps/alsijil/models.py
index 277e2bfe03d6495767cf27c15782f627f7eccf2d..0b651945f31604f09230fad7739ccfb17902a9d1 100644
--- a/aleksis/apps/alsijil/models.py
+++ b/aleksis/apps/alsijil/models.py
@@ -71,7 +71,7 @@ class ExcuseType(ExtensibleModel):
 
     @property
     def count_label(self):
-        return f"{self.short_name}_count"
+        return f"excuse_type_{self.id}_count"
 
     class Meta:
         ordering = ["name"]
@@ -250,7 +250,7 @@ class PersonalNote(RegisterObjectRelatedMixin, ExtensibleModel):
     )
 
     absent = models.BooleanField(default=False)
-    late = models.PositiveSmallIntegerField(default=0)
+    tardiness = models.PositiveSmallIntegerField(default=0)
     excused = models.BooleanField(default=False)
     excuse_type = models.ForeignKey(
         ExcuseType,
@@ -282,7 +282,7 @@ class PersonalNote(RegisterObjectRelatedMixin, ExtensibleModel):
         defaults = PersonalNote()
 
         self.absent = defaults.absent
-        self.late = defaults.late
+        self.tardiness = defaults.tardiness
         self.excused = defaults.excused
         self.excuse_type = defaults.excuse_type
         self.remarks = defaults.remarks
@@ -450,7 +450,7 @@ class ExtraMark(ExtensibleModel):
 
     @property
     def count_label(self):
-        return f"{self.short_name}_count"
+        return f"extra_mark_{self.id}_count"
 
     class Meta:
         ordering = ["short_name"]
diff --git a/aleksis/apps/alsijil/static/css/alsijil/alsijil.css b/aleksis/apps/alsijil/static/css/alsijil/alsijil.css
index 3e14ca014661f0f80d1df639d02949db6ffb59e9..a30fb99bb8b239ca38f7f4d627329594387741c6 100644
--- a/aleksis/apps/alsijil/static/css/alsijil/alsijil.css
+++ b/aleksis/apps/alsijil/static/css/alsijil/alsijil.css
@@ -9,52 +9,52 @@ table a.tr-link {
 }
 
 .collapsible-icon-right {
-	align-self: end;
-	flex-grow: 100;
-	text-align: right!important;
+  align-self: end;
+  flex-grow: 100;
+  text-align: right !important;
 }
 
 @media only screen and (min-width: 1201px) {
-    .hide-on-extra-large-only {
-        display: none;
-    }
+  .hide-on-extra-large-only {
+    display: none;
+  }
 }
 
 @media only screen and (max-width: 1200px) {
-    .show-on-extra-large {
-        display: none;
-    }
+  .show-on-extra-large {
+    display: none;
+  }
 }
 
 @media only screen and (max-width: 600px) {
-    .collection .collection-item.avatar {
-        padding-left: 20px;
-    }
-    .collection .collection-item.avatar:not(.circle-clipper) > .circle {
-        position: relative;
-        margin-bottom: 10px;
-    }
+  .collection .collection-item.avatar {
+    padding-left: 20px;
+  }
+  .collection .collection-item.avatar:not(.circle-clipper) > .circle {
+    position: relative;
+    margin-bottom: 10px;
+  }
 }
 
 .collapsible li .show-on-active {
-    display: none;
+  display: none;
 }
 
 .collapsible li.active .show-on-active {
-    display: block;
+  display: block;
 }
 
 th.chip-height {
-    height: 67px;
-    line-height: 2.2;
+  height: 67px;
+  line-height: 2.2;
 }
 
 .collection-item.chip-height {
-    height: 52px;
-    line-height: 2.2;
+  height: 52px;
+  line-height: 2.2;
 }
 
 li.collection-item.button-height {
-    height: 58px;
-    line-height: 2.5;
+  height: 58px;
+  line-height: 2.5;
 }
diff --git a/aleksis/apps/alsijil/static/css/alsijil/full_register.css b/aleksis/apps/alsijil/static/css/alsijil/full_register.css
index 9068a104cb2464a0cb95073e3ee8a66bddfaf21e..533c84326887b2050cb66edadcf92706f64b963d 100644
--- a/aleksis/apps/alsijil/static/css/alsijil/full_register.css
+++ b/aleksis/apps/alsijil/static/css/alsijil/full_register.css
@@ -1,72 +1,76 @@
-table.small-print, td.small-print, th.small-print {
-    font-size: 10pt;
+table.small-print,
+td.small-print,
+th.small-print {
+  font-size: 10pt;
 }
 
 #signatures {
-    padding-top: 3em;
+  padding-top: 3em;
 }
 
 #signatures .signature {
-    display: inline-block;
-    width: 12em;
-    border-top: 1px solid;
-    margin-right: 20px;
+  display: inline-block;
+  width: 12em;
+  border-top: 1px solid;
+  margin-right: 20px;
 }
 
 tr {
-    border-bottom: 1px solid rgba(0, 0, 0, 0.3);
+  border-bottom: 1px solid rgba(0, 0, 0, 0.3);
 }
 
-td, th {
-    padding: 1px;
+td,
+th {
+  padding: 1px;
 }
 
 tr.lessons-day-first {
-    border-top: 3px solid rgba(0, 0, 0, 0.3);
+  border-top: 3px solid rgba(0, 0, 0, 0.3);
 }
 
-td.rotate, th.rotate {
-    text-align: center;
-    transform: rotate(-90deg);
+td.rotate,
+th.rotate {
+  text-align: center;
+  transform: rotate(-90deg);
 }
 
 tr.lesson-cancelled td {
-    background-color: #EF9A9A;
+  background-color: #ef9a9a;
 }
 
 tr.lesson-substituted td {
-    background-color: #ffb74d;
+  background-color: #ffb74d;
 }
 
 td.lesson-notes {
-    font-size: 80%;
+  font-size: 80%;
 }
 
 td.lesson-notes span.lesson-note-absent {
-    color: #cc0000;
+  color: #cc0000;
 }
 
 td.lesson-notes span.lesson-note-late {
-    color: #ff9933;
+  color: #ff9933;
 }
 
 td.lesson-notes span.lesson-note-excused {
-    color: #009933;
+  color: #009933;
 }
 
 table.person-info {
-    border: none;
+  border: none;
 }
 
 table.person-info td.person-img {
-    text-align: center;
+  text-align: center;
 }
 
 table.person-info td.person-img img {
-    max-height: 30mm;
+  max-height: 30mm;
 }
 
 img.max-size-600 {
-    max-width: 600px;
-    max-height: 600px;
+  max-width: 600px;
+  max-height: 600px;
 }
diff --git a/aleksis/apps/alsijil/static/css/alsijil/lesson.css b/aleksis/apps/alsijil/static/css/alsijil/lesson.css
index cb2d9399c9d865c7116c2f8137274edc1ec501d0..48937d33d5a763d9d8ac6c4dbdfa4c9eae573ac3 100644
--- a/aleksis/apps/alsijil/static/css/alsijil/lesson.css
+++ b/aleksis/apps/alsijil/static/css/alsijil/lesson.css
@@ -1,133 +1,138 @@
 .alsijil-check-box {
-    margin-right: 10px;
+  margin-right: 10px;
 }
 
 .alsijil-check-box [type="checkbox"] {
-    padding-left: 30px;
+  padding-left: 30px;
 }
 
 .alsijil-lesson-cancelled {
-    text-decoration: line-through;
+  text-decoration: line-through;
 }
 
-.alsijil-tardiness-text{
-    vertical-align: super;
+.alsijil-tardiness-text {
+  vertical-align: super;
 }
 
-
 @media only screen and (max-width: 992px) {
-    .no-mobile-card {
-        border: unset;
-        padding: unset;
-        margin: unset;
-        box-shadow: unset;
-    }
-    .no-mobile-card .card-content {
-        padding: unset;
-    }
-    table.alsijil-table.horizontal-on-small {
-        display: block;
-        max-width: calc(100vw - 40px);
-    }
-    table.alsijil-table.horizontal-on-small thead {
-        display: none;
-    }
-    table.alsijil-table.horizontal-on-small tbody {
-        overflow-x: scroll;
-        display: flex;
-        column-gap: 1rem;
-        flex-wrap: nowrap;
-        align-items: stretch;
-        scroll-snap-type: x proximity;
-    }
-
-    table.alsijil-table.horizontal-on-small tr {
-        flex-basis: min(75vw, 400px);
-        flex-shrink: 0;
-        flex-grow: 1;
-        border-radius: 8px;
-        display: flex;
-        flex-direction: column;
-        justify-content: space-between;
-        scroll-snap-align: center;
-        transition: all .5s;
-        margin: 0.5rem 0 1rem 0;
-        background-color: #fff!important;
-    	box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 3px 1px -2px rgba(0, 0, 0, 0.12), 0 1px 5px 0 rgba(0, 0, 0, 0.2);
-        padding: 24px;
-    }
-    table.alsijil-table.horizontal-on-small tr:first-of-type {
-        margin-inline-start: .4rem;
-        -moz-margin-start: .4rem;
-        -webkit-margin-start: .4rem;
-    }
-
-    table.alsijil-table.horizontal-on-small tr:last-of-type {
-        margin-inline-end: .4rem;
-        -moz-margin-end: .4rem;
-        -webkit-margin-end: .4rem;
-    }
-    table.alsijil-table.horizontal-on-small td.center-align {
-        text-align: left;
-    }
-    table.alsijil-table.horizontal-on-small .person-name {
-        font-size: 24px;
-        font-weight: 300;
-        display: block;
-        line-height: 32px;
-        margin-bottom: 8px;
-    }
-}
-
-.alsijil-time-head, .alsijil-object-head {
+  .no-mobile-card {
+    border: unset;
+    padding: unset;
+    margin: unset;
+    box-shadow: unset;
+  }
+  .no-mobile-card .card-content {
+    padding: unset;
+  }
+  table.alsijil-table.horizontal-on-small {
+    display: block;
+    max-width: calc(100vw - 40px);
+  }
+  table.alsijil-table.horizontal-on-small thead {
+    display: none;
+  }
+  table.alsijil-table.horizontal-on-small tbody {
+    overflow-x: scroll;
+    display: flex;
+    column-gap: 1rem;
+    flex-wrap: nowrap;
+    align-items: stretch;
+    scroll-snap-type: x proximity;
+  }
+
+  table.alsijil-table.horizontal-on-small tr {
+    flex-basis: min(75vw, 400px);
+    flex-shrink: 0;
+    flex-grow: 1;
+    border-radius: 8px;
+    display: flex;
+    flex-direction: column;
+    justify-content: space-between;
+    scroll-snap-align: center;
+    transition: all 0.5s;
+    margin: 0.5rem 0 1rem 0;
+    background-color: #fff !important;
+    box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14),
+      0 3px 1px -2px rgba(0, 0, 0, 0.12), 0 1px 5px 0 rgba(0, 0, 0, 0.2);
+    padding: 24px;
+  }
+  table.alsijil-table.horizontal-on-small tr:first-of-type {
+    margin-inline-start: 0.4rem;
+    -moz-margin-start: 0.4rem;
+    -webkit-margin-start: 0.4rem;
+  }
+
+  table.alsijil-table.horizontal-on-small tr:last-of-type {
+    margin-inline-end: 0.4rem;
+    -moz-margin-end: 0.4rem;
+    -webkit-margin-end: 0.4rem;
+  }
+  table.alsijil-table.horizontal-on-small td.center-align {
+    text-align: left;
+  }
+  table.alsijil-table.horizontal-on-small .person-name {
+    font-size: 24px;
+    font-weight: 300;
     display: block;
+    line-height: 32px;
+    margin-bottom: 8px;
+  }
+}
+
+.alsijil-time-head,
+.alsijil-object-head {
+  display: block;
 }
 
 .alsijil-time-head {
-    font-size: 2rem;
-    line-height: 1.1;
+  font-size: 2rem;
+  line-height: 1.1;
 }
 
 .alsijil-object-head {
-    font-size: 3rem;
+  font-size: 3rem;
 }
 
 @media only screen and (max-width: 600px) {
-    .alsijil-time-head {
-        font-size: 1.5rem;
-    }
-
-    .alsijil-object-head {
-        font-size: 2.2rem;
-        line-height: 1.4;
-    }
+  .alsijil-time-head {
+    font-size: 1.5rem;
+  }
+
+  .alsijil-object-head {
+    font-size: 2.2rem;
+    line-height: 1.4;
+  }
 }
 
 .alsijil-nav {
-    line-height: 36px;
+  line-height: 36px;
 }
 
 .alsijil-header-nav-button {
-    height: 66px;
-    padding: 0;
+  height: 66px;
+  padding: 0;
 }
 
 .alsijil-header-nav-button.left {
-    margin-right: 5px;
+  margin-right: 5px;
 }
 
 .alsijil-header-nav-button.right {
-    margin-left: 5px;
+  margin-left: 5px;
 }
 
 .alsijil-header-nav-button i.material-icons {
-    line-height: 60px;
-    height: 60px;
-    font-size: 40px;
+  line-height: 60px;
+  height: 60px;
+  font-size: 40px;
 }
 
 .alsijil-nav-header {
-    width: calc(100% + 40px);
-    padding: 10px 20px;
-    margin: -10px -20px 0;
-}
\ No newline at end of file
+  width: calc(100% + 40px);
+  padding: 10px 20px;
+  margin: -10px -20px 0;
+}
+
+.tabs-icons .tab svg.iconify {
+  display: block;
+}
diff --git a/aleksis/apps/alsijil/static/css/alsijil/person.css b/aleksis/apps/alsijil/static/css/alsijil/person.css
index b5a59aae95235f696a06d0641215b1392dd5b2a3..d385d7b69e031fafd4c21d037b70b8f973e99b74 100644
--- a/aleksis/apps/alsijil/static/css/alsijil/person.css
+++ b/aleksis/apps/alsijil/static/css/alsijil/person.css
@@ -1,101 +1,100 @@
 span.input-field.inline > .select-wrapper > input {
-    color: red;
-    padding: 14px 0 0 0;
-    line-height: 2px;
-    height: 36px;
-    vertical-align: middle;
+  color: red;
+  padding: 14px 0 0 0;
+  line-height: 2px;
+  height: 36px;
+  vertical-align: middle;
 }
 
 span.input-field.inline > .select-wrapper .caret {
-    top: 12px !important;
+  top: 12px !important;
 }
 
 @media screen and (min-width: 1400px) {
-    li.collection-item form {
-        margin: -30px 0 -30px 0;
-    }
+  li.collection-item form {
+    margin: -30px 0 -30px 0;
+  }
 
-    li.collection-item#title #select_all_span {
-        margin-top: 5px;
-    }
+  li.collection-item#title #select_all_span {
+    margin-top: 5px;
+  }
 }
 
 .collection {
-    overflow: visible;
-    overflow-x: hidden;
+  overflow: visible;
+  overflow-x: hidden;
 }
 
 #select_all_container {
-    display: none;
+  display: none;
 }
 
 #select_all_box:indeterminate + span:not(.lever):before {
-    top: -4px;
-    left: -6px;
-    width: 10px;
-    height: 12px;
-    border-top: none;
-    border-left: none;
-    border-right: white 2px solid;
-    border-bottom: none;
-    transform: rotate(90deg);
-    backface-visibility: hidden;
-    transform-origin: 100% 100%;
-
+  top: -4px;
+  left: -6px;
+  width: 10px;
+  height: 12px;
+  border-top: none;
+  border-left: none;
+  border-right: white 2px solid;
+  border-bottom: none;
+  transform: rotate(90deg);
+  backface-visibility: hidden;
+  transform-origin: 100% 100%;
 }
 
 #select_all_box:indeterminate + span:not(.lever):after {
-    top: 0;
-    width: 20px;
-    height: 20px;
-    border: 2px solid currentColor;
-    background-color: currentColor;
-    z-index: 0;
+  top: 0;
+  width: 20px;
+  height: 20px;
+  border: 2px solid currentColor;
+  background-color: currentColor;
+  z-index: 0;
 }
 
 #select_all_box_text {
-    color: #9e9e9e !important;
+  color: #9e9e9e !important;
 }
 
 td.material-icons {
-    display: table-cell;
+  display: table-cell;
 }
 
 .medium-high {
-    position: relative;
-    top: 50%;
-    left: 50%;
-    transform: translate(-50%, 50%);
+  position: relative;
+  top: 50%;
+  left: 50%;
+  transform: translate(-50%, 50%);
 }
 
 @media screen and (min-width: 600px) {
-    /* On medium and up devices */
-    .medium-high-right {
-        float: right;
-        transform: translate(0%, 50%);
-    }
+  /* On medium and up devices */
+  .medium-high-right {
+    float: right;
+    transform: translate(0%, 50%);
+  }
 }
 
 @media screen and (max-width: 600px) {
-    /* Only on small devices */
-    .full-width-s {
-        width: 100%;
-    }
-
-    #heading {
-        display: block;
-    }
-    #heading + a {
-        float: none!important;
-    }
+  /* Only on small devices */
+  .full-width-s {
+    width: 100%;
+  }
+
+  #heading {
+    display: block;
+  }
+  #heading + a {
+    float: none !important;
+  }
 }
 
 .overflow-x-scroll {
-    overflow-x: scroll;
+  overflow-x: scroll;
 }
 
 figure.modal-content figcaption {
-    font-weight: 300;
-    font-size: 2.28rem;
-    line-height: 110%;
+  font-weight: 300;
+  font-size: 2.28rem;
+  line-height: 110%;
 }
diff --git a/aleksis/apps/alsijil/static/css/alsijil/week_view.css b/aleksis/apps/alsijil/static/css/alsijil/week_view.css
index ccd1595e0fa5ad3f3c10ee8857c644723892e6f2..a42111f55e31d3b3f518873218c76fd246106afe 100644
--- a/aleksis/apps/alsijil/static/css/alsijil/week_view.css
+++ b/aleksis/apps/alsijil/static/css/alsijil/week_view.css
@@ -1,116 +1,122 @@
 @media screen and (max-width: 600px) {
-    #toggle-row button[type=submit] {
-        width: 100%;
-        margin-bottom: 1em;
-    }
+  #toggle-row button[type="submit"] {
+    width: 100%;
+    margin-bottom: 1em;
+  }
 }
 
 .horizontal-scroll-container {
-    overflow-x: scroll;
-    display: flex;
-    column-gap: 1rem;
-    flex-wrap: nowrap;
-    align-items: stretch;
-    scroll-snap-type: x proximity;
+  overflow-x: scroll;
+  display: flex;
+  column-gap: 1rem;
+  flex-wrap: nowrap;
+  align-items: stretch;
+  scroll-snap-type: x proximity;
 }
 
 .horizontal-scroll-container.vertical {
-    flex-wrap: wrap;
-    overflow-x: inherit;
+  flex-wrap: wrap;
+  overflow-x: inherit;
 }
 
 .horizontal-scroll-container.vertical .horizontal-scroll-card {
-    margin-inline: 0;
+  margin-inline: 0;
 }
 
 dl {
-    margin: 0;
-    padding: 0;
+  margin: 0;
+  padding: 0;
 }
 
 dt {
-    font-weight: bold;
+  font-weight: bold;
 }
 
 dd {
-    margin: 0;
-    padding: unset;
+  margin: 0;
+  padding: unset;
 }
 
 .horizontal-scroll-card {
-    flex-basis: min(75vw, 400px);
-    flex-shrink: 0;
-    flex-grow: 1;
-    border-radius: 8px;
-    display: flex;
-    flex-direction: column;
-    justify-content: space-between;
-    scroll-snap-align: center;
-    transition: all .5s;
+  flex-basis: min(75vw, 400px);
+  flex-shrink: 0;
+  flex-grow: 1;
+  border-radius: 8px;
+  display: flex;
+  flex-direction: column;
+  justify-content: space-between;
+  scroll-snap-align: center;
+  transition: all 0.5s;
 }
 
 .horizontal-scroll-card:first-of-type {
-    margin-inline-start: .4rem;
-    -moz-margin-start: .4rem;
-    -webkit-margin-start: .4rem;
+  margin-inline-start: 0.4rem;
+  -moz-margin-start: 0.4rem;
+  -webkit-margin-start: 0.4rem;
 }
 
 .horizontal-scroll-card:last-of-type {
-    margin-inline-end: .4rem;
-    -moz-margin-end: .4rem;
-    -webkit-margin-end: .4rem;
+  margin-inline-end: 0.4rem;
+  -moz-margin-end: 0.4rem;
+  -webkit-margin-end: 0.4rem;
 }
 
 .horizontal-scroll-card .card-action {
-    margin-bottom: 5px;
+  margin-bottom: 5px;
 }
 
 .horizontal-scroll-card .card-content .card-title {
-    display: flex;
-    justify-content: space-between;
+  display: flex;
+  justify-content: space-between;
 }
 
 .horizontal-scroll-card .card-content .card-title .subject {
-    flex-grow: 5;
+  flex-grow: 5;
 }
 
 .horizontal-scroll-card .one-line {
-   display: grid;
-    grid-auto-flow: column;
-    grid-template-rows: 1fr 1fr;
+  display: grid;
+  grid-auto-flow: column;
+  grid-template-rows: 1fr 1fr;
 }
 
 p.subtitle {
-    display: flex;
-    justify-content: space-between;
-    align-items: flex-end;
+  display: flex;
+  justify-content: space-between;
+  align-items: flex-end;
 }
 
 .btn-superflat ~ span {
-    line-height: 24px;
+  line-height: 24px;
 }
 
-.btn-superflat, .btn-superflat:focus, .btn-superflat:active {
-    border: none;
-    line-height: 1;
-    height: 24px;
-    background: none;
-    font-weight: normal;
+.btn-superflat,
+.btn-superflat:focus,
+.btn-superflat:active {
+  border: none;
+  line-height: 1;
+  height: 24px;
+  background: none;
+  font-weight: normal;
 }
 
 .btn-superflat i.material-icons {
-    vertical-align: middle;
+  vertical-align: middle;
 }
 
 .btn-superflat:hover {
-    cursor: pointer;
+  cursor: pointer;
 }
 
 .unfold-trigger i.material-icons {
-    transition: transform .5s 0s ease-in-out;
-    transform: rotate(-90deg);
+  transition: transform 0.5s 0s ease-in-out;
+  transform: rotate(-90deg);
 }
 
 .unfold-trigger.vertical i.material-icons {
-    transform: rotate(-180deg);
+  transform: rotate(-180deg);
+}
+
+.tabs-icons .tab svg.iconify {
+  display: block;
 }
diff --git a/aleksis/apps/alsijil/static/js/alsijil/week_view.js b/aleksis/apps/alsijil/static/js/alsijil/week_view.js
index c5daa0451b63a63aac4e8b7703beaaa6d8b26fe5..69124b9c41e656948bbced89b06b8d4edaf3b3c2 100644
--- a/aleksis/apps/alsijil/static/js/alsijil/week_view.js
+++ b/aleksis/apps/alsijil/static/js/alsijil/week_view.js
@@ -1,21 +1,20 @@
 $(document).ready(function () {
-    $("#id_group").change(function () {
-        $("#id_teacher").val("").formSelect();
-    });
-    $("#id_teacher").change(function () {
-        $("#id_group").val("").formSelect();
-    });
-    $("#toggle-row.pre-hidden").hide();
-
+  $("#id_group").change(function () {
+    $("#id_teacher").val("").formSelect();
+  });
+  $("#id_teacher").change(function () {
+    $("#id_group").val("").formSelect();
+  });
+  $("#toggle-row.pre-hidden").hide();
 });
 $("#toggle-button").click(function () {
-    $("#toggle-row").toggle();
-})
+  $("#toggle-row").toggle();
+});
 $(".unfold-trigger").click(function (event) {
-    let target = event.target;
-    target.classList.toggle("vertical");
-    let next_container = $(target).parent().next(".horizontal-scroll-container");
-    if (next_container.length >= 1) {
-        next_container[0].classList.toggle("vertical");
-    }
-})
\ No newline at end of file
+  let target = event.target;
+  target.classList.toggle("vertical");
+  let next_container = $(target).parent().next(".horizontal-scroll-container");
+  if (next_container.length >= 1) {
+    next_container[0].classList.toggle("vertical");
+  }
+});
diff --git a/aleksis/apps/alsijil/tables.py b/aleksis/apps/alsijil/tables.py
index c82385265b37d6b37f365048b2ad8c4b6ae52cf1..a0ef1734e5eea326fd9a8fd0f858b71a89089082 100644
--- a/aleksis/apps/alsijil/tables.py
+++ b/aleksis/apps/alsijil/tables.py
@@ -115,7 +115,7 @@ class PersonalNoteTable(tables.Table):
     )
     subject = tables.Column(verbose_name=_("Subject"), accessor=A("subject"), linkify=True)
     absent = tables.Column(verbose_name=_("Absent"))
-    late = tables.Column(verbose_name=_("Tardiness"))
+    tardiness = tables.Column(verbose_name=_("Tardiness"))
     excused = tables.Column(verbose_name=_("Excuse"))
     extra_marks = tables.Column(verbose_name=_("Extra marks"), accessor=A("extra_marks__all"))
 
@@ -151,9 +151,9 @@ class PersonalNoteTable(tables.Table):
             return badge
         return "–"
 
-    def render_late(self, value):
+    def render_tardiness(self, value):
         if value:
-            content = _(f"{value}' late")
+            content = _(f"{value}' tardiness")
             context = dict(content=content, classes="orange white-text")
             return render_to_string("components/materialize-chips.html", context)
         else:
diff --git a/aleksis/apps/alsijil/tasks.py b/aleksis/apps/alsijil/tasks.py
index aa8de7b5a075b9a1ae37b129b1870482966577a5..7eaf8c01a5186ef08c12a5e4b19e9fc1e853d4d2 100644
--- a/aleksis/apps/alsijil/tasks.py
+++ b/aleksis/apps/alsijil/tasks.py
@@ -87,6 +87,11 @@ def generate_full_register_printout(group: int, file_object: int, recorder: Prog
             day = event.date_start + timedelta(days=i)
             event_copy = deepcopy(event)
             event_copy.annotate_day(day)
+
+            # Skip event days if it isn't inside the timetable schema
+            if not (event_copy.raw_period_from_on_day and event_copy.raw_period_to_on_day):
+                continue
+
             register_objects_by_day.setdefault(day, []).append(
                 (
                     event_copy,
diff --git a/aleksis/apps/alsijil/templates/alsijil/class_register/lesson.html b/aleksis/apps/alsijil/templates/alsijil/class_register/lesson.html
index fd376568067136e72107ec5f9363b4dc68ab4040..056bea93ca036f54869ad037f84b7575f3a2123d 100644
--- a/aleksis/apps/alsijil/templates/alsijil/class_register/lesson.html
+++ b/aleksis/apps/alsijil/templates/alsijil/class_register/lesson.html
@@ -3,7 +3,6 @@
 {% load week_helpers material_form_internal material_form i18n static rules time_helpers %}
 
 {% block browser_title %}{% blocktrans %}Lesson{% endblocktrans %}{% endblock %}
-{% block no_page_title %}{% endblock %}
 {% block extra_head %}
   {{ block.super }}
   <link rel="stylesheet" href="{% static 'css/alsijil/lesson.css' %}"/>
@@ -13,48 +12,8 @@
   {% endif %}
 {% endblock %}
 
-{% block nav_content %}
-  <ul class="tabs tabs-transparent tabs-icons tabs-fixed-width">
-    <li class="tab">
-      <a href="#lesson-documentation">
-        <i class="material-icons iconify" data-icon="mdi:message-bulleted"></i>
-        {% trans "Period" %}
-      </a>
-    </li>
-    {% if register_object.label_ != "lesson_period" or not register_object.get_substitution.cancelled or not request.site.preferences.alsijil__block_personal_notes_for_cancelled %}
-      <li class="tab">
-        <a href="#personal-notes">
-          <i class="material-icons iconify" data-icon="mdi:account-multiple-outline"></i>
-          {% trans "Persons" %}
-        </a>
-      </li>
-    {% endif %}
-    {% if with_seating_plan %}
-      <li class="tab">
-        <a href="#seating-plan">
-          <i class="material-icons iconify" data-icon="mdi:seat-outline"></i>
-          {% trans "Seating plan" %}
-        </a>
-      </li>
-    {% endif %}
-    {% if prev_lesson %}
-      {% has_perm "alsijil.view_lessondocumentation_rule" user prev_lesson as can_view_prev_lesson_documentation %}
-      {% if prev_lesson.get_lesson_documentation and can_view_prev_lesson_documentation %}
-        <li class="tab">
-          <a href="#previous-lesson">
-            <i class="material-icons iconify" data-icon="mdi:history"></i>
-            {% trans "Previous" %}
-          </a>
-        </li>
-      {% endif %}
-    {% endif %}
-    <li class="tab">
-      <a href="#more">
-        <i class="material-icons iconify" data-icon="mdi:dots-horizontal"></i>
-        {% trans "More" %}
-      </a>
-    </li>
-  </ul>
+{% block page_title %}
+  {% include "alsijil/partials/lesson/heading.html" %}
 {% endblock %}
 
 {% block content %}
@@ -62,54 +21,50 @@
   {% has_perm "alsijil.edit_lessondocumentation_rule" user register_object as can_edit_lesson_documentation %}
   {% has_perm "alsijil.edit_register_object_personalnote_rule" user register_object as can_edit_register_object_personalnote %}
 
-  {% if next_lesson_person or prev_lesson_person or back_to_week_url %}
-    <div class="row margin-bottom z-depth-1 alsijil-nav-header">
-      <div class="col s12 no-padding">
-        {# Back to week view #}
-        {% if back_to_week_url %}
-          <a href="{{ back_to_week_url }}"
-             class="btn secondary-color waves-light waves-effect margin-bottom {% if prev_lesson_person or next_lesson_person %}hide-on-extra-large-only{% endif %}">
-            <i class="material-icons iconify left" data-icon="mdi:chevron-left"></i> {% trans "Week view" %}
+  <!-- Tab Buttons -->
+  <div class="col s12 margin-bottom">
+    <ul class="tabs tabs-icons tabs-fixed-width">
+      <li class="tab col">
+        <a href="#lesson-documentation">
+          <i class="material-icons iconify" data-icon="mdi:message-bulleted"></i>
+          {% trans "Period" %}
+        </a>
+      </li>
+      {% if register_object.label_ != "lesson_period" or not register_object.get_substitution.cancelled or not request.site.preferences.alsijil__block_personal_notes_for_cancelled %}
+        <li class="tab col">
+          <a href="#personal-notes">
+            <i class="material-icons iconify" data-icon="mdi:account-multiple-outline"></i>
+            {% trans "Persons" %}
           </a>
-        {% endif %}
-
-        {% if prev_lesson_person or next_lesson_person %}
-          <div class="col s12 no-padding center alsijil-nav">
-            {% if back_to_week_url %}
-              <a href="{{ back_to_week_url }}"
-                 class="btn-flat secondary-color-text waves-light waves-effect left hide-on-med-and-down hide-on-large-only show-on-extra-large">
-                <i class="material-icons iconify left" data-icon="mdi:chevron-left"></i> {% trans "Week view" %}
-              </a>
-            {% endif %}
-
-            {# Previous lesson #}
-            <a class="btn-flat waves-effect waves-light left primary-color-text {% if not prev_lesson_person %}disabled{% endif %}"
-               title="{% trans "My previous lesson" %}"
-                {% if prev_lesson_person %}
-               href="{% url "lesson_period" prev_lesson_person.week.year prev_lesson_person.week.week prev_lesson_person.id %}"
-                {% endif %}
-            >
-              <i class="material-icons iconify left" data-icon="mdi:chevron-left"></i>
-              <span class="hide-on-small-only">{% trans "My previous lesson" %}</span>
-              <span class="hide-on-med-and-up">{% trans "Previous" %}</span>
-            </a>
-            {# Next lesson #}
-            <a class="btn-flat waves-effect waves-light right primary-color-text {% if not next_lesson_person %}disabled{% endif %}"
-               title="{% trans "My next lesson" %}"
-                {% if next_lesson_person %}
-               href="{% url "lesson_period" next_lesson_person.week.year next_lesson_person.week.week next_lesson_person.id %}"
-                {% endif %}
-            >
-              <i class="material-icons iconify right" data-icon="mdi:chevron-right"></i>
-              <span class="hide-on-small-only">{% trans "My next lesson" %}</span>
-              <span class="hide-on-med-and-up">{% trans "Next" %}</span>
+        </li>
+      {% endif %}
+      {% if with_seating_plan %}
+        <li class="tab col">
+          <a href="#seating-plan">
+            <i class="material-icons iconify" data-icon="mdi:seat-outline"></i>
+            {% trans "Seating plan" %}
+          </a>
+        </li>
+      {% endif %}
+      {% if prev_lesson %}
+        {% has_perm "alsijil.view_lessondocumentation_rule" user prev_lesson as can_view_prev_lesson_documentation %}
+        {% if prev_lesson.get_lesson_documentation and can_view_prev_lesson_documentation %}
+          <li class="tab col">
+            <a href="#previous-lesson">
+              <i class="material-icons iconify" data-icon="mdi:history"></i>
+              {% trans "Previous" %}
             </a>
-            <span class="truncate">{{ request.user.person }}</span>
-          </div>
+          </li>
         {% endif %}
-      </div>
-    </div>
-  {% endif %}
+      {% endif %}
+      <li class="tab col">
+        <a href="#more">
+          <i class="material-icons iconify" data-icon="mdi:dots-horizontal"></i>
+          {% trans "More" %}
+        </a>
+      </li>
+    </ul>
+  </div>
 
   <form method="post" class="row">
     {% csrf_token %}
@@ -148,8 +103,6 @@
         </div>
       </div>
     {% else %}
-      {% include "alsijil/partials/lesson/heading.html" %}
-
       <div class="row no-margin">
         <div class="container">
           <div class="card">
diff --git a/aleksis/apps/alsijil/templates/alsijil/class_register/week_view.html b/aleksis/apps/alsijil/templates/alsijil/class_register/week_view.html
index ce38ee671b6e51a9f4417cb8507dc530b2b81a48..d8c8273092fee51086fac6b24599da95fef486d0 100644
--- a/aleksis/apps/alsijil/templates/alsijil/class_register/week_view.html
+++ b/aleksis/apps/alsijil/templates/alsijil/class_register/week_view.html
@@ -11,35 +11,6 @@
   <link rel="stylesheet" href="{% static 'css/alsijil/week_view.css' %}"/>
 {% endblock %}
 
-{% block nav_content %}
-  {% if lesson_periods %}
-    <div class="">
-      <ul class="tabs tabs-transparent tabs-icons tabs-fixed-width">
-        <li class="tab col">
-          <a class="active" href="#week-overview">
-            <i class="material-icons iconify" data-icon="mdi:message-bulleted"></i>
-            {% trans "Lesson documentations" %}
-          </a>
-        </li>
-        <li class="tab col">
-          <a href="#personal-notes">
-            <i class="material-icons iconify" data-icon="mdi:account-multiple-outline"></i>
-            {% trans "Persons" %}
-          </a>
-        </li>
-        {% if group_roles %}
-          <li class="tab col">
-            <a href="#group-roles">
-              <i class="material-icons iconify" data-icon="mdi:clipboard-account-outline"></i>
-              {% trans "Group roles" %}
-            </a>
-          </li>
-        {% endif %}
-      </ul>
-    </div>
-  {% endif %}
-{% endblock %}
-
 {% block content %}
   <script type="text/javascript" src="{% static "js/helper.js" %}"></script>
   {{ week_select|json_script:"week_select" }}
@@ -97,6 +68,33 @@
     </p>
   {% endif %}
 
+  {% if lesson_periods %}
+  <div class="col s12 margin-bottom">
+    <ul class="tabs tabs-icons tabs-fixed-width">
+      <li class="tab col">
+        <a class="active" href="#week-overview">
+          <i class="material-icons iconify" data-icon="mdi:message-bulleted"></i>
+          {% trans "Lesson documentations" %}
+        </a>
+      </li>
+      <li class="tab col">
+        <a href="#personal-notes">
+          <i class="material-icons iconify" data-icon="mdi:account-multiple-outline"></i>
+          {% trans "Persons" %}
+        </a>
+      </li>
+      {% if group_roles %}
+        <li class="tab col">
+          <a href="#group-roles">
+            <i class="material-icons iconify" data-icon="mdi:clipboard-account-outline"></i>
+            {% trans "Group roles" %}
+          </a>
+        </li>
+      {% endif %}
+    </ul>
+  </div>
+  {% endif %}
+
   {% if lesson_periods %}
     <div class="row">
       <div class="col s12" id="week-overview">
diff --git a/aleksis/apps/alsijil/templates/alsijil/partials/lesson/heading.html b/aleksis/apps/alsijil/templates/alsijil/partials/lesson/heading.html
index 4b768265a510df03c6fab59b40cb4e356f90290a..132e97f05acd0216d59f89a12cab08b96e123cc5 100644
--- a/aleksis/apps/alsijil/templates/alsijil/partials/lesson/heading.html
+++ b/aleksis/apps/alsijil/templates/alsijil/partials/lesson/heading.html
@@ -1,5 +1,54 @@
 {% load i18n %}
 
+{% if next_lesson_person or prev_lesson_person or back_to_week_url %}
+  <div class="row margin-bottom alsijil-nav-header">
+    <div class="col s12 no-padding">
+      {# Back to week view #}
+      {% if back_to_week_url %}
+        <a href="{{ back_to_week_url }}"
+           class="btn secondary-color waves-light waves-effect margin-bottom {% if prev_lesson_person or next_lesson_person %}hide-on-extra-large-only{% endif %}">
+          <i class="material-icons iconify left" data-icon="mdi:chevron-left"></i> {% trans "Week view" %}
+        </a>
+      {% endif %}
+
+      {% if prev_lesson_person or next_lesson_person %}
+        <div class="col s12 no-padding center alsijil-nav">
+          {% if back_to_week_url %}
+            <a href="{{ back_to_week_url }}"
+               class="btn-flat secondary-color-text waves-light waves-effect left hide-on-med-and-down hide-on-large-only show-on-extra-large">
+              <i class="material-icons iconify left" data-icon="mdi:chevron-left"></i> {% trans "Week view" %}
+            </a>
+          {% endif %}
+
+          {# Previous lesson #}
+          <a class="btn-flat waves-effect waves-light left primary-color-text {% if not prev_lesson_person %}disabled{% endif %}"
+             title="{% trans "My previous lesson" %}"
+              {% if prev_lesson_person %}
+             href="{% url "lesson_period" prev_lesson_person.week.year prev_lesson_person.week.week prev_lesson_person.id %}"
+              {% endif %}
+          >
+            <i class="material-icons iconify left" data-icon="mdi:chevron-left"></i>
+            <span class="hide-on-small-only">{% trans "My previous lesson" %}</span>
+            <span class="hide-on-med-and-up">{% trans "Previous" %}</span>
+          </a>
+          {# Next lesson #}
+          <a class="btn-flat waves-effect waves-light right primary-color-text {% if not next_lesson_person %}disabled{% endif %}"
+             title="{% trans "My next lesson" %}"
+              {% if next_lesson_person %}
+             href="{% url "lesson_period" next_lesson_person.week.year next_lesson_person.week.week next_lesson_person.id %}"
+              {% endif %}
+          >
+            <i class="material-icons iconify right" data-icon="mdi:chevron-right"></i>
+            <span class="hide-on-small-only">{% trans "My next lesson" %}</span>
+            <span class="hide-on-med-and-up">{% trans "Next" %}</span>
+          </a>
+          <span class="truncate">{{ request.user.person }}</span>
+        </div>
+      {% endif %}
+    </div>
+  </div>
+{% endif %}
+
 <h1>
   <span class="right hide-on-small-only">
     {% include "alsijil/partials/lesson_status.html" with register_object=register_object css_class="medium" %}
diff --git a/aleksis/apps/alsijil/templates/alsijil/partials/lesson/tabs/documentation.html b/aleksis/apps/alsijil/templates/alsijil/partials/lesson/tabs/documentation.html
index 7a67a23ed9a2c3a2f08d299f344b583fc13ffe24..98b18fe153a8bb8f292ebe6ea20286df357680f0 100644
--- a/aleksis/apps/alsijil/templates/alsijil/partials/lesson/tabs/documentation.html
+++ b/aleksis/apps/alsijil/templates/alsijil/partials/lesson/tabs/documentation.html
@@ -1,6 +1,5 @@
 {% load i18n material_form_internal material_form data_helpers %}
 
-{% include "alsijil/partials/lesson/heading.html" %}
 {% include "alsijil/partials/lesson/prev_next.html" with with_save=0 %}
 
 <div class="hide-on-med-and-up margin-bottom">
diff --git a/aleksis/apps/alsijil/templates/alsijil/partials/lesson/tabs/more.html b/aleksis/apps/alsijil/templates/alsijil/partials/lesson/tabs/more.html
index b7e010125077dd003e529bb85432dfc64adad7ab..ffc7706488757871e43de4e71da9e73802273a3f 100644
--- a/aleksis/apps/alsijil/templates/alsijil/partials/lesson/tabs/more.html
+++ b/aleksis/apps/alsijil/templates/alsijil/partials/lesson/tabs/more.html
@@ -1,7 +1,5 @@
 {% load i18n %}
 
-{% include "alsijil/partials/lesson/heading.html" %}
-
 {% if group_roles %}
   {% include "alsijil/group_role/partials/assigned_roles.html" with roles=group_roles group=register_object.get_groups.first back_url=back_url %}
 {% endif %}
diff --git a/aleksis/apps/alsijil/templates/alsijil/partials/lesson/tabs/notes.html b/aleksis/apps/alsijil/templates/alsijil/partials/lesson/tabs/notes.html
index c1a1e0030ddf7b28a77362a3dbe3637ddb879357..1b013252a8f316979cdf5bdcc87ef1c7463b6e36 100644
--- a/aleksis/apps/alsijil/templates/alsijil/partials/lesson/tabs/notes.html
+++ b/aleksis/apps/alsijil/templates/alsijil/partials/lesson/tabs/notes.html
@@ -1,6 +1,5 @@
 {% load i18n material_form_internal material_form time_helpers %}
 
-{% include "alsijil/partials/lesson/heading.html" %}
 {% include "alsijil/partials/lesson/prev_next.html" with with_save=1 %}
 
 {% if not blocked_because_holidays %}
@@ -54,7 +53,7 @@
             </td>
             <td>
               <div class="input-field">
-                {{ form.late }}
+                {{ form.tardiness }}
                 <label for="{{ form.absent.id_for_label }}">
                   {% trans "Tardiness (in m)" %}
                 </label>
@@ -109,9 +108,9 @@
               <i class="material-icons iconify center" data-icon="mdi:{{ form.absent.value|yesno:"check,close" }}"></i>
             </td>
             <td>
-              <i class="material-icons iconify center" data-icon="mdi:{{ form.late.value|yesno:"check,close" }}"></i>
+              <i class="material-icons iconify center" data-icon="mdi:{{ form.tardiness.value|yesno:"check,close" }}"></i>
               <span class="alsijil-tardiness-text">
-                {% if form.late.value %}{{ form.late.value|to_time|time:"i\m" }}{% endif %}
+                {% if form.tardiness.value %}{{ form.tardiness.value|to_time|time:"i\m" }}{% endif %}
               </span>
             </td>
             <td>
diff --git a/aleksis/apps/alsijil/templates/alsijil/partials/persons_with_stats.html b/aleksis/apps/alsijil/templates/alsijil/partials/persons_with_stats.html
index bb60dbbd0984d1dab0130ada052cb423288aee2e..69ce9c6c9a011744eb2b3bed80228fe3147ff01b 100644
--- a/aleksis/apps/alsijil/templates/alsijil/partials/persons_with_stats.html
+++ b/aleksis/apps/alsijil/templates/alsijil/partials/persons_with_stats.html
@@ -12,11 +12,13 @@
     <th rowspan="2">{% trans "Name" %}</th>
     <th rowspan="2">{% trans "Primary group" %}</th>
     <th colspan="{{ excuse_types.count|add:4 }}">{% trans "Absences" %}</th>
-    <th colspan="{{ excuse_types_not_absent.count }}">{% trans "Uncounted Absences" %}</th>
-    <th rowspan="2">{% trans "Tardiness" %}</th>
+    {% if excuse_types_not_absent %}
+      <th colspan="{{ excuse_types_not_absent.count }}">{% trans "Uncounted Absences" %}</th>
+    {% endif %}
     {% if extra_marks %}
       <th colspan="{{ extra_marks.count }}">{% trans "Extra marks" %}</th>
     {% endif %}
+    <th rowspan="2">{% trans "Tardiness" %}</th>
     <th rowspan="2"></th>
   </tr>
   <tr class="hide-on-large-only">
@@ -36,12 +38,12 @@
         ({{ excuse_type.short_name }})
       </th>
     {% endfor %}
-    <th class="truncate chip-height">{% trans "Tardiness" %}</th>
     {% for extra_mark in extra_marks %}
       <th class="chip-height">
         {{ extra_mark.short_name }}
       </th>
     {% endfor %}
+    <th class="truncate chip-height">{% trans "Tardiness" %}</th>
     <th rowspan="2"></th>
   </tr>
   <tr class="hide-on-med-and-down">
@@ -110,12 +112,6 @@
           </span>
         </td>
       {% endfor %}
-      <td>
-        <span class="chip orange white-text" title="{% trans "Tardiness" %}">
-          {% firstof person.tardiness|to_time|time:"H\h i\m"  "–" %}
-        </span>
-        <span class="chip orange white-text" title="{% trans "Count of tardiness" %}">{{ person.tardiness_count }} &times;</span>
-      </td>
       {% for extra_mark in extra_marks %}
         <td>
           <span class="chip grey white-text" title="{{ extra_mark.name }}">
@@ -123,6 +119,12 @@
           </span>
         </td>
       {% endfor %}
+      <td>
+        <span class="chip orange white-text" title="{% trans "Tardiness" %}">
+          {% firstof person.tardiness|to_time|time:"H\h i\m"  "–" %}
+        </span>
+        <span class="chip orange white-text" title="{% trans "Count of tardiness" %}">{{ person.tardiness_count }} &times;</span>
+      </td>
 
       <td>
         <a class="btn primary waves-effect waves-light" href="{% url "overview_person" person.pk %}">
diff --git a/aleksis/apps/alsijil/templates/alsijil/partials/tardinesses.html b/aleksis/apps/alsijil/templates/alsijil/partials/tardinesses.html
index d11a94ea16f4ebd23a41dce65aca7980e1893172..ca20a9c4d1f420736febd149e657a67e855d45f4 100644
--- a/aleksis/apps/alsijil/templates/alsijil/partials/tardinesses.html
+++ b/aleksis/apps/alsijil/templates/alsijil/partials/tardinesses.html
@@ -2,6 +2,6 @@
 {% for note in notes %}
   {% has_perm "alsijil.view_personalnote_rule" user note as can_view_personalnote %}
   {% if can_view_personalnote %}
-    <span>{{ note.person }} ({{ note.late }}'){% if not forloop.last %},{% endif %}</span>
+    <span>{{ note.person }} ({{ note.tardiness }}'){% if not forloop.last %},{% endif %}</span>
   {% endif %}
 {% endfor %}
diff --git a/aleksis/apps/alsijil/templates/alsijil/print/full_register.html b/aleksis/apps/alsijil/templates/alsijil/print/full_register.html
index c8d71ff3f6b2162814f72a83cfbc14fa8c6f0b1b..c699287a00987ad99c1fd0c2d4617827670fae2f 100644
--- a/aleksis/apps/alsijil/templates/alsijil/print/full_register.html
+++ b/aleksis/apps/alsijil/templates/alsijil/print/full_register.html
@@ -357,7 +357,7 @@
 
       <tbody>
       {% for note in person.filtered_notes %}
-        {% if note.absent or note.late or note.remarks or note.extra_marks.all %}
+        {% if note.absent or note.tardiness or note.remarks or note.extra_marks.all %}
           <tr>
             {% if note.date %}
               <td>{{ note.date }}</td>
@@ -389,8 +389,8 @@
               {% endif %}
             </td>
             <td>
-              {% if note.late %}
-                {{ note.late }}'
+              {% if note.tardiness %}
+                {{ note.tardiness }}'
               {% endif %}
             </td>
             <td>
@@ -486,10 +486,10 @@
                       {% endif %}
                     </span>
                   {% endif %}
-                  {% if note.late %}
+                  {% if note.tardiness %}
                     <span class="lesson-note-late">
                       {{ note.person.last_name }}, {{ note.person.first_name|slice:"0:1" }}.
-                      ({{ note.late }}′)
+                      ({{ note.tardiness }}′)
                       {% if note.excused %}
                         <span class="lesson-note-excused">
                           {% if note.excuse_type %}
diff --git a/aleksis/apps/alsijil/tests/test_actions.py b/aleksis/apps/alsijil/tests/test_actions.py
index 6f1bd6ef4ce3900ddc6ea7dac5df6f3a25dd90fc..8dd499ac953583637a3ade36c835aa0e5d6aae24 100644
--- a/aleksis/apps/alsijil/tests/test_actions.py
+++ b/aleksis/apps/alsijil/tests/test_actions.py
@@ -52,7 +52,7 @@ def _prepare_notes():
             excused=True,
             excuse_type=excuse_type,
         ),
-        PersonalNote(person=person, event=_generate_event(date(2021, 10, 4)), late=10),
+        PersonalNote(person=person, event=_generate_event(date(2021, 10, 4)), tardiness=10),
         PersonalNote(
             person=person, event=_generate_event(date(2032, 10, 11)), remarks="Good work!"
         ),
diff --git a/aleksis/apps/alsijil/views.py b/aleksis/apps/alsijil/views.py
index 9a333b3c1e2af83677f7edff6dd53959c566e49b..0208255e5b05f2c56cb1cd7e17cda5e4921545aa 100644
--- a/aleksis/apps/alsijil/views.py
+++ b/aleksis/apps/alsijil/views.py
@@ -15,7 +15,7 @@ from django.urls import reverse, reverse_lazy
 from django.utils import timezone
 from django.utils.decorators import method_decorator
 from django.utils.http import url_has_allowed_host_and_scheme
-from django.utils.translation import ugettext as _
+from django.utils.translation import gettext as _
 from django.views import View
 from django.views.decorators.cache import never_cache
 from django.views.generic import DetailView
@@ -33,6 +33,7 @@ from aleksis.apps.chronos.managers import TimetableType
 from aleksis.apps.chronos.models import Event, ExtraLesson, Holiday, LessonPeriod, TimePeriod
 from aleksis.apps.chronos.util.build import build_weekdays
 from aleksis.apps.chronos.util.date import get_weeks_for_year, week_weekday_to_date
+from aleksis.core.decorators import pwa_cache
 from aleksis.core.mixins import (
     AdvancedCreateView,
     AdvancedDeleteView,
@@ -42,7 +43,7 @@ from aleksis.core.mixins import (
 from aleksis.core.models import Group, PDFFile, Person, SchoolTerm
 from aleksis.core.util import messages
 from aleksis.core.util.celery_progress import render_progress_page
-from aleksis.core.util.core_helpers import get_site_preferences, objectgetter_optional
+from aleksis.core.util.core_helpers import get_site_preferences, has_person, objectgetter_optional
 from aleksis.core.util.predicates import check_global_permission
 
 from .filters import InstructionFilter, PersonalNoteFilter
@@ -88,6 +89,7 @@ from .util.alsijil_helpers import (
 )
 
 
+@pwa_cache
 @permission_required("alsijil.view_register_object_rule", fn=get_register_object_by_pk)  # FIXME
 def register_object(
     request: HttpRequest,
@@ -248,8 +250,7 @@ def register_object(
             "alsijil.view_register_object_personalnote_rule", register_object
         ):
             persons = Person.objects.filter(
-                Q(pk=request.user.person.pk)
-                | Q(person__member_of__in=request.user.person.owner_of.all())
+                Q(pk=request.user.person.pk) | Q(member_of__in=request.user.person.owner_of.all())
             ).distinct()
         else:
             persons = Person.objects.all()
@@ -344,6 +345,7 @@ def register_object(
     return render(request, "alsijil/class_register/lesson.html", context)
 
 
+@pwa_cache
 @permission_required("alsijil.view_week_rule", fn=get_timetable_instance_by_pk)
 def week_view(
     request: HttpRequest,
@@ -554,10 +556,10 @@ def week_view(
                     filtered_personal_notes__absent=True, filtered_personal_notes__excused=False
                 ),
             ),
-            tardiness_sum=Sum("filtered_personal_notes__late"),
+            tardiness_sum=Sum("filtered_personal_notes__tardiness"),
             tardiness_count=Count(
                 "filtered_personal_notes",
-                filter=Q(filtered_personal_notes__late__gt=0),
+                filter=Q(filtered_personal_notes__tardiness__gt=0),
             ),
         )
 
@@ -649,6 +651,7 @@ def week_view(
     return render(request, "alsijil/class_register/week_view.html", context)
 
 
+@pwa_cache
 @permission_required(
     "alsijil.view_full_register_rule", fn=objectgetter_optional(Group, None, False)
 )
@@ -656,8 +659,11 @@ def full_register_group(request: HttpRequest, id_: int) -> HttpResponse:
     group = get_object_or_404(Group, pk=id_)
 
     file_object = PDFFile.objects.create()
+    if has_person(request):
+        file_object.person = request.user.person
+        file_object.save()
 
-    redirect_url = reverse("redirect_to_pdf_file", args=[file_object.pk])
+    redirect_url = f"/pdfs/{file_object.pk}"
 
     result = generate_full_register_printout.delay(group.pk, file_object.pk)
 
@@ -685,6 +691,7 @@ def full_register_group(request: HttpRequest, id_: int) -> HttpResponse:
     )
 
 
+@pwa_cache
 @permission_required("alsijil.view_my_students_rule")
 def my_students(request: HttpRequest) -> HttpResponse:
     context = {}
@@ -725,6 +732,7 @@ def my_students(request: HttpRequest) -> HttpResponse:
     return render(request, "alsijil/class_register/persons.html", context)
 
 
+@pwa_cache
 @permission_required(
     "alsijil.view_my_groups_rule",
 )
@@ -736,6 +744,7 @@ def my_groups(request: HttpRequest) -> HttpResponse:
     return render(request, "alsijil/class_register/groups.html", context)
 
 
+@method_decorator(pwa_cache, "dispatch")
 class StudentsList(PermissionRequiredMixin, DetailView):
     model = Group
     template_name = "alsijil/class_register/students_list.html"
@@ -755,6 +764,7 @@ class StudentsList(PermissionRequiredMixin, DetailView):
         return context
 
 
+@pwa_cache
 @permission_required(
     "alsijil.view_person_overview_rule",
     fn=objectgetter_optional(
@@ -802,7 +812,7 @@ def overview_person(request: HttpRequest, id_: Optional[int] = None) -> HttpResp
 
     personal_notes = (
         allowed_personal_notes.not_empty()
-        .filter(Q(absent=True) | Q(late__gt=0) | ~Q(remarks="") | Q(extra_marks__isnull=False))
+        .filter(Q(absent=True) | Q(tardiness__gt=0) | ~Q(remarks="") | Q(extra_marks__isnull=False))
         .annotate(
             school_term_start=Case(
                 When(event__isnull=False, then="event__school_term__date_start"),
@@ -922,8 +932,10 @@ def overview_person(request: HttpRequest, id_: Optional[int] = None) -> HttpResp
                     unexcused=Count("absent")
                 )
             )
-            stat.update(personal_notes.aggregate(tardiness=Sum("late")))
-            stat.update(personal_notes.filter(~Q(late=0)).aggregate(tardiness_count=Count("late")))
+            stat.update(personal_notes.aggregate(tardiness=Sum("tardiness")))
+            stat.update(
+                personal_notes.filter(~Q(tardiness=0)).aggregate(tardiness_count=Count("tardiness"))
+            )
 
             for extra_mark in extra_marks:
                 stat.update(
@@ -1064,6 +1076,7 @@ class DeletePersonalNoteView(PermissionRequiredMixin, DetailView):
         return redirect("overview_person", note.person.pk)
 
 
+@method_decorator(pwa_cache, "dispatch")
 class ExtraMarkListView(PermissionRequiredMixin, SingleTableView):
     """Table of all extra marks."""
 
@@ -1108,6 +1121,7 @@ class ExtraMarkDeleteView(PermissionRequiredMixin, RevisionMixin, AdvancedDelete
     success_message = _("The extra mark has been deleted.")
 
 
+@method_decorator(pwa_cache, "dispatch")
 class ExcuseTypeListView(PermissionRequiredMixin, SingleTableView):
     """Table of all excuse types."""
 
@@ -1152,6 +1166,7 @@ class ExcuseTypeDeleteView(PermissionRequiredMixin, RevisionMixin, AdvancedDelet
     success_message = _("The excuse type has been deleted.")
 
 
+@method_decorator(pwa_cache, "dispatch")
 class GroupRoleListView(PermissionRequiredMixin, SingleTableView):
     """Table of all group roles."""
 
@@ -1196,6 +1211,7 @@ class GroupRoleDeleteView(PermissionRequiredMixin, RevisionMixin, AdvancedDelete
     success_message = _("The group role has been deleted.")
 
 
+@method_decorator(pwa_cache, "dispatch")
 class AssignedGroupRolesView(PermissionRequiredMixin, DetailView):
     permission_required = "alsijil.view_assigned_grouproles_rule"
     model = Group
@@ -1318,6 +1334,7 @@ class GroupRoleAssignmentDeleteView(
         return reverse("assigned_group_roles", args=[pk])
 
 
+@method_decorator(pwa_cache, "dispatch")
 class AllRegisterObjectsView(PermissionRequiredMixin, View):
     """Provide overview of all register objects for coordinators."""
 
diff --git a/docs/conf.py b/docs/conf.py
index 59ca213449c1f46b86ef5340aaac7c21447cb4e8..b37b0f814f16734100037dfe13bc1d8a4e007456 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -29,9 +29,9 @@ copyright = "2019-2022 The AlekSIS team"
 author = "The AlekSIS Team"
 
 # The short X.Y version
-version = "2.1"
+version = "3.0"
 # The full version, including alpha/beta/rc tags
-release = "2.2.dev0"
+release = "3.0.1.dev0"
 
 
 # -- General configuration ---------------------------------------------------
diff --git a/pyproject.toml b/pyproject.toml
index a3a4aea8d9b8f3cfd286cab366900bcce9ee5b9c..759cac17c8e175968e8fa17228d9204188321da6 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
 [tool.poetry]
 name = "AlekSIS-App-Alsijil"
-version = "2.2.dev0"
+version = "3.0.1.dev0"
 packages = [
     { include = "aleksis" }
 ]
@@ -24,36 +24,33 @@ authors = [
     "mirabilos <thorsten.glaser@teckids.org>",
     "Tom Teichler <tom.teichler@teckids.org>"
 ]
-maintainers = [
-    "Dominik George <dominik.george@teckids.org>",
-    "Jonathan Weth <dev@jonathanweth.de>",
-]
-license = "EUPL-1.2"
-homepage = "https://aleksis.edugit.io/"
-repository = "https://edugit.org/AlekSIS/Official/AlekSIS-App-Alsijil"
+maintainers = ["Jonathan Weth <dev@jonathanweth.de>", "Dominik George <dominik.george@teckids.org>"]
+license = "EUPL-1.2-or-later"
+homepage = "https://aleksis.org"
+repository = "https://edugit.org/AlekSIS/official/AlekSIS-App-Alsijil"
 documentation = "https://aleksis.edugit.io/AlekSIS/docs/html/"
 classifiers = [
-    "Development Status :: 4 - Beta",
+    "Development Status :: 5 - Production/Stable",
     "Environment :: Web Environment",
-    "Framework :: Django :: 3.0",
+    "Framework :: Django :: 4.0",
     "Intended Audience :: Education",
     "Topic :: Education",
     "Typing :: Typed",
 ]
 
+[[tool.poetry.source]]
+name = "PyPI"
+priority = "primary"
+
 [[tool.poetry.source]]
 name = "gitlab"
 url = "https://edugit.org/api/v4/projects/461/packages/pypi/simple"
-secondary = true
-
+priority = "supplemental"
 [tool.poetry.dependencies]
 python = "^3.9"
-aleksis-core = "^2.12"
-aleksis-app-chronos = "^2.2"
-aleksis-app-stoelindeling = { version = "^1.0", optional = true }
-
-[tool.poetry.dev-dependencies]
-aleksis-builddeps = "*"
+aleksis-core = "^3.0"
+aleksis-app-chronos = "^3.0"
+aleksis-app-stoelindeling = { version = "^2.0", optional = true }
 
 [tool.poetry.extras]
 seatingplans = ["aleksis-app-stoelindeling"]
@@ -61,10 +58,52 @@ seatingplans = ["aleksis-app-stoelindeling"]
 [tool.poetry.plugins."aleksis.app"]
 alsijil = "aleksis.apps.alsijil.apps:AlsijilConfig"
 
+
+[tool.poetry.group.dev.dependencies]
+django-stubs = "^4.2"
+
+safety = "^2.3.5"
+
+flake8 = "^6.0.0"
+flake8-django = "~1.2.0"
+flake8-fixme = "^1.1.1"
+flake8-mypy = "^17.8.0"
+flake8-bandit = "^4.1.1"
+flake8-builtins = "^2.0.0"
+flake8-docstrings = "^1.5.0"
+flake8-rst-docstrings = "^0.3.0"
+
+black = ">=21.0"
+flake8-black = "^0.3.0"
+
+isort = "^5.0.0"
+flake8-isort = "^6.0.0"
+
+curlylint = "^0.13.0"
+
+[tool.poetry.group.test.dependencies]
+pytest = "^7.2"
+pytest-django = "^4.1"
+pytest-django-testing-postgresql = "^0.2"
+pytest-cov = "^4.0.0"
+pytest-sugar = "^0.9.2"
+selenium = "<4.10.0"
+freezegun = "^1.1.0"
+
+[tool.poetry.group.docs]
+optional = true
+
+[tool.poetry.group.docs.dependencies]
+sphinx = "^7.0"
+sphinxcontrib-django = "^2.3.0"
+sphinxcontrib-svg2pdfconverter = "^1.1.1"
+sphinx-autodoc-typehints = "^1.7"
+sphinx_material = "^0.0.35"
+
 [tool.black]
 line-length = 100
 exclude = "/migrations/"
 
 [build-system]
-requires = ["poetry>=1.0"]
-build-backend = "poetry.masonry.api"
+requires = ["poetry-core>=1.0.0"]
+build-backend = "poetry.core.masonry.api"
diff --git a/tox.ini b/tox.ini
index 78e09567e193b72a299dfb3f776499420ecc04ec..66c9e773824ab52d5c0e5574016b83696d5736dc 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,16 +1,15 @@
 [tox]
 skipsdist = True
 skip_missing_interpreters = true
-envlist = py37,py38,py39
+envlist = py39,py310,py311
 
 [testenv]
-whitelist_externals = poetry
-		      sudo
+allowlist_externals = poetry
 skip_install = true
 envdir = {toxworkdir}/globalenv
 commands_pre =
      poetry install
-     poetry run aleksis-admin webpack_bundle
+     poetry run aleksis-admin vite build
      poetry run aleksis-admin collectstatic --no-input
 commands =
     poetry run pytest --cov=. {posargs} aleksis/
@@ -27,6 +26,8 @@ commands =
     poetry run black --check --diff aleksis/
     poetry run isort -c --diff --stdout aleksis/
     poetry run flake8 {posargs} aleksis/
+    poetry run sh -c "aleksis-admin yarn run prettier --check --ignore-path={toxinidir}/.prettierignore {toxinidir}"
+    poetry run sh -c "aleksis-admin yarn run eslint {toxinidir}/aleksis/**/*/frontend/**/*.{js,vue} --config={toxinidir}/.eslintrc.js --resolve-plugins-relative-to=."
 
 [testenv:security]
 commands =
@@ -46,6 +47,7 @@ commands = poetry run make -C docs/ html {posargs}
 commands =
     poetry run isort aleksis/
     poetry run black aleksis/
+    poetry run sh -c "aleksis-admin yarn run prettier --write --ignore-path={toxinidir}/.prettierignore {toxinidir}"
 
 [testenv:makemessages]
 commands =