From 25c430fbfc3dc1b8d921eddd63f5ecf81b6337dc Mon Sep 17 00:00:00 2001
From: Hangzhi Yu <hangzhi@protonmail.com>
Date: Thu, 26 Jan 2023 13:58:25 +0100
Subject: [PATCH] Add SPA support

---
 CHANGELOG.rst                                 |   5 +
 aleksis/apps/alsijil/frontend/index.js        | 393 ++++++++++++++++++
 .../apps/alsijil/frontend/messages/de.json    |   5 +
 .../apps/alsijil/frontend/messages/en.json    |  36 ++
 .../alsijil/class_register/lesson.html        | 128 ++----
 .../alsijil/partials/lesson/heading.html      |  49 +++
 .../partials/lesson/tabs/documentation.html   |   1 -
 .../alsijil/partials/lesson/tabs/more.html    |   2 -
 .../alsijil/partials/lesson/tabs/notes.html   |   1 -
 aleksis/apps/alsijil/views.py                 |  13 +
 10 files changed, 539 insertions(+), 94 deletions(-)
 create mode 100644 aleksis/apps/alsijil/frontend/index.js
 create mode 100644 aleksis/apps/alsijil/frontend/messages/de.json
 create mode 100644 aleksis/apps/alsijil/frontend/messages/en.json

diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 6e62cd43e..89a1735d4 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -9,6 +9,11 @@ and this project adheres to `Semantic Versioning`_.
 Unreleased
 ----------
 
+Added
+~~~~~
+
+* Add SPA support.
+
 Changed
 ~~~~~~~
 
diff --git a/aleksis/apps/alsijil/frontend/index.js b/aleksis/apps/alsijil/frontend/index.js
new file mode 100644
index 000000000..14bb71e6f
--- /dev/null
+++ b/aleksis/apps/alsijil/frontend/index.js
@@ -0,0 +1,393 @@
+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: "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",
+        },
+      },
+      ],
+  }
diff --git a/aleksis/apps/alsijil/frontend/messages/de.json b/aleksis/apps/alsijil/frontend/messages/de.json
new file mode 100644
index 000000000..527bebf46
--- /dev/null
+++ b/aleksis/apps/alsijil/frontend/messages/de.json
@@ -0,0 +1,5 @@
+{
+  "alsijil": {
+    "menu_title": "Klassenbuch"
+  }
+}
diff --git a/aleksis/apps/alsijil/frontend/messages/en.json b/aleksis/apps/alsijil/frontend/messages/en.json
new file mode 100644
index 000000000..cd9798229
--- /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/templates/alsijil/class_register/lesson.html b/aleksis/apps/alsijil/templates/alsijil/class_register/lesson.html
index fd3765680..550a87ace 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,45 @@
   {% 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">
+      <li class="tab">
+        <a href="#lesson-documentation">
+          {% 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">
+            {% 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">
+          <a href="#seating-plan">
+            {% 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">
+              {% trans "Previous" %}
             </a>
-            <span class="truncate">{{ request.user.person }}</span>
-          </div>
+          </li>
         {% endif %}
-      </div>
-    </div>
-  {% endif %}
+      {% endif %}
+      <li class="tab">
+        <a href="#more">
+          {% trans "More" %}
+        </a>
+      </li>
+    </ul>
+  </div>
 
   <form method="post" class="row">
     {% csrf_token %}
@@ -148,8 +98,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/partials/lesson/heading.html b/aleksis/apps/alsijil/templates/alsijil/partials/lesson/heading.html
index 4b768265a..2a641ff53 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">
+    <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 ef0f42048..22b396f45 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 %}
 
-{% 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 b7e010125..ffc770648 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 e94fb66d7..1b013252a 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 %}
diff --git a/aleksis/apps/alsijil/views.py b/aleksis/apps/alsijil/views.py
index 487a27d3b..618cf99b1 100644
--- a/aleksis/apps/alsijil/views.py
+++ b/aleksis/apps/alsijil/views.py
@@ -31,6 +31,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,
@@ -77,6 +78,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,
@@ -327,6 +329,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,
@@ -632,6 +635,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)
 )
@@ -668,6 +672,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 = {}
@@ -708,6 +713,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",
 )
@@ -719,6 +725,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"
@@ -738,6 +745,7 @@ class StudentsList(PermissionRequiredMixin, DetailView):
         return context
 
 
+@pwa_cache
 @permission_required(
     "alsijil.view_person_overview_rule",
     fn=objectgetter_optional(
@@ -1049,6 +1057,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."""
 
@@ -1093,6 +1102,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."""
 
@@ -1137,6 +1147,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."""
 
@@ -1181,6 +1192,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
@@ -1303,6 +1315,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."""
 
-- 
GitLab