diff --git a/aleksis/apps/alsijil/forms.py b/aleksis/apps/alsijil/forms.py
index 9624e5208e614319527a95b33feae1fea4e84b6b..5d0c161296a1258e6fbb65fd4192dfcc4bc84cc4 100644
--- a/aleksis/apps/alsijil/forms.py
+++ b/aleksis/apps/alsijil/forms.py
@@ -15,7 +15,7 @@ from aleksis.apps.chronos.models import TimePeriod
 from aleksis.core.models import Group, Person
 from aleksis.core.util.predicates import check_global_permission
 
-from .models import ExcuseType, ExtraMark, LessonDocumentation, PersonalNote
+from .models import ClassRole, ExcuseType, ExtraMark, LessonDocumentation, PersonalNote
 
 
 class LessonDocumentationForm(forms.ModelForm):
@@ -162,3 +162,11 @@ class ExcuseTypeForm(forms.ModelForm):
     class Meta:
         model = ExcuseType
         fields = ["short_name", "name"]
+
+
+class ClassRoleForm(forms.ModelForm):
+    layout = Layout("name", "icon", "colour")
+
+    class Meta:
+        model = ClassRole
+        fields = ["name", "icon", "colour"]
diff --git a/aleksis/apps/alsijil/menus.py b/aleksis/apps/alsijil/menus.py
index f90052a765147b8555840d84f24203b12f09b248..a0ac455ca38fe9b566a846e0dd726d7c8e4cd000 100644
--- a/aleksis/apps/alsijil/menus.py
+++ b/aleksis/apps/alsijil/menus.py
@@ -89,6 +89,17 @@ MENUS = {
                         ),
                     ],
                 },
+                {
+                    "name": _("Manage class roles"),
+                    "url": "class_roles",
+                    "icon": "support_agent",
+                    "validators": [
+                        (
+                            "aleksis.core.util.predicates.permission_validator",
+                            "alsijil.view_classroles",
+                        ),
+                    ],
+                },
             ],
         }
     ]
diff --git a/aleksis/apps/alsijil/preferences.py b/aleksis/apps/alsijil/preferences.py
index 4ced04f41ba56db628b4dc4438242a81bc36f5eb..055d5c9971247bac3dd99d7b86350950c115a1e6 100644
--- a/aleksis/apps/alsijil/preferences.py
+++ b/aleksis/apps/alsijil/preferences.py
@@ -66,3 +66,11 @@ class AllowEntriesInHolidays(BooleanPreference):
     name = "allow_entries_in_holidays"
     default = False
     verbose_name = _("Allow teachers to add data for lessons in holidays")
+
+
+@site_preferences_registry.register
+class ActivateClassRoles(BooleanPreference):
+    section = alsijil
+    name = "activate_class_roles"
+    default = True
+    verbose_name = _("Activate support for creating and assigning class roles")
diff --git a/aleksis/apps/alsijil/rules.py b/aleksis/apps/alsijil/rules.py
index f553c2773ef6e96aab87b585d01c389399cecd50..0d74385a39710ea6f3589ea722646866fbb4c53a 100644
--- a/aleksis/apps/alsijil/rules.py
+++ b/aleksis/apps/alsijil/rules.py
@@ -218,3 +218,26 @@ add_perm("alsijil.edit_extramark", edit_extramark_predicate)
 # Delete extra mark
 delete_extramark_predicate = view_extramarks_predicate & has_global_perm("alsijil.delete_extramark")
 add_perm("alsijil.delete_extramark", delete_extramark_predicate)
+
+
+# View class role list
+view_class_roles_predicate = (
+    has_person
+    & is_site_preference_set("alsijil", "activate_class_roles")
+    & has_global_perm("alsijil.view_classrole")
+)
+add_perm("alsijil.view_classroles", view_class_roles_predicate)
+
+# Add class role
+add_class_role_predicate = view_class_roles_predicate & has_global_perm("alsijil.add_classrole")
+add_perm("alsijil.add_classrole", add_class_role_predicate)
+
+# Edit class role
+edit_class_role_predicate = view_class_roles_predicate & has_global_perm("alsijil.change_classrole")
+add_perm("alsijil.edit_classrole", edit_class_role_predicate)
+
+# Delete class role
+delete_class_role_predicate = view_class_roles_predicate & has_global_perm(
+    "alsijil.delete_classrole"
+)
+add_perm("alsijil.delete_classrole", delete_class_role_predicate)
diff --git a/aleksis/apps/alsijil/tables.py b/aleksis/apps/alsijil/tables.py
index b9a8e68404d6b2672dfbb37d2433e55cad08cf08..0cfdc1ab2009792fb99c2e3ab8c03a289f2489d6 100644
--- a/aleksis/apps/alsijil/tables.py
+++ b/aleksis/apps/alsijil/tables.py
@@ -1,3 +1,4 @@
+from django.template.loader import render_to_string
 from django.utils.translation import gettext_lazy as _
 
 import django_tables2 as tables
@@ -48,3 +49,34 @@ class ExcuseTypeTable(tables.Table):
             self.columns.hide("edit")
         if not request.user.has_perm("alsijil.delete_excusetype"):
             self.columns.hide("delete")
+
+
+class ClassRoleTable(tables.Table):
+    class Meta:
+        attrs = {"class": "highlight"}
+
+    name = tables.LinkColumn("edit_excuse_type", args=[A("id")])
+    edit = tables.LinkColumn(
+        "edit_class_role",
+        args=[A("id")],
+        text=_("Edit"),
+        attrs={"a": {"class": "btn-flat waves-effect waves-orange orange-text"}},
+    )
+    delete = tables.LinkColumn(
+        "delete_class_role",
+        args=[A("id")],
+        text=_("Delete"),
+        attrs={"a": {"class": "btn-flat waves-effect waves-red red-text"}},
+    )
+
+    def render_name(self, value, record):
+        colour = record.colour or "black"
+        icon_name = record.icon or "support_agent"
+        context = dict(content=value, icon=icon_name, classes=f"{colour} white-text")
+        return render_to_string("components/materialize-chips.html", context)
+
+    def before_render(self, request):
+        if not request.user.has_perm("alsijil.edit_classrole"):
+            self.columns.hide("edit")
+        if not request.user.has_perm("alsijil.delete_classrole"):
+            self.columns.hide("delete")
diff --git a/aleksis/apps/alsijil/templates/alsijil/class_role/create.html b/aleksis/apps/alsijil/templates/alsijil/class_role/create.html
new file mode 100644
index 0000000000000000000000000000000000000000..096d34ed94ec488dfc6dbb6df4c7a2d4cf9652f5
--- /dev/null
+++ b/aleksis/apps/alsijil/templates/alsijil/class_role/create.html
@@ -0,0 +1,15 @@
+{# -*- engine:django -*- #}
+
+{% extends "core/base.html" %}
+{% load material_form i18n %}
+
+{% block browser_title %}{% blocktrans %}Create class role{% endblocktrans %}{% endblock %}
+{% block page_title %}{% blocktrans %}Create class role{% endblocktrans %}{% endblock %}
+
+{% block content %}
+  <form method="post">
+    {% csrf_token %}
+    {% form form=form %}{% endform %}
+    {% include "core/partials/save_button.html" %}
+  </form>
+{% endblock %}
diff --git a/aleksis/apps/alsijil/templates/alsijil/class_role/edit.html b/aleksis/apps/alsijil/templates/alsijil/class_role/edit.html
new file mode 100644
index 0000000000000000000000000000000000000000..b01aa3dc95eda1ba07556ccbf7e14aa6ed253de0
--- /dev/null
+++ b/aleksis/apps/alsijil/templates/alsijil/class_role/edit.html
@@ -0,0 +1,17 @@
+{# -*- engine:django -*- #}
+
+{% extends "core/base.html" %}
+{% load material_form i18n %}
+
+{% block browser_title %}{% blocktrans %}Edit class role{% endblocktrans %}{% endblock %}
+{% block page_title %}{% blocktrans %}Edit class role{% endblocktrans %}{% endblock %}
+
+{% block content %}
+
+  <form method="post">
+    {% csrf_token %}
+    {% form form=form %}{% endform %}
+    {% include "core/partials/save_button.html" %}
+  </form>
+
+{% endblock %}
diff --git a/aleksis/apps/alsijil/templates/alsijil/class_role/list.html b/aleksis/apps/alsijil/templates/alsijil/class_role/list.html
new file mode 100644
index 0000000000000000000000000000000000000000..146b6501355e4797adcd3050647eae83d3b699f0
--- /dev/null
+++ b/aleksis/apps/alsijil/templates/alsijil/class_role/list.html
@@ -0,0 +1,22 @@
+{# -*- engine:django -*- #}
+
+{% extends "core/base.html" %}
+
+{% load i18n rules %}
+{% load render_table from django_tables2 %}
+
+{% block browser_title %}{% blocktrans %}Class roles{% endblocktrans %}{% endblock %}
+{% block page_title %}{% blocktrans %}Class roles{% endblocktrans %}{% endblock %}
+
+{% block content %}
+  {% has_perm "alsijil.add_classrole" user as add_class_role %}
+  {% if add_class_role %}
+    <a class="btn green waves-effect waves-light" href="{% url 'create_class_role' %}">
+      <i class="material-icons left">add</i>
+      {% trans "Create class role" %}
+    </a>
+  {% endif %}
+
+  {% render_table table %}
+{% endblock %}
+
diff --git a/aleksis/apps/alsijil/templates/alsijil/class_role/warning.html b/aleksis/apps/alsijil/templates/alsijil/class_role/warning.html
new file mode 100644
index 0000000000000000000000000000000000000000..d90d2e8205b1c91c18e74e02654fde3daebc4971
--- /dev/null
+++ b/aleksis/apps/alsijil/templates/alsijil/class_role/warning.html
@@ -0,0 +1,10 @@
+{% load i18n %}
+<div class="alert warning">
+  <p>
+    <i class="material-icons left">warning</i>
+    {% blocktrans %}
+      This function should only be used to define alternatives to the default excuse which also will be counted extra.
+      Don't use this to create a default excuse or if you don't divide between different types of excuse.
+    {% endblocktrans %}
+  </p>
+</div>
diff --git a/aleksis/apps/alsijil/urls.py b/aleksis/apps/alsijil/urls.py
index 673154f496b081b194b95cdde9aeef8464f05519..752f5b8e9598016be0bf835c4f5ca20190af83fb 100644
--- a/aleksis/apps/alsijil/urls.py
+++ b/aleksis/apps/alsijil/urls.py
@@ -49,4 +49,12 @@ urlpatterns = [
         views.ExcuseTypeDeleteView.as_view(),
         name="delete_excuse_type",
     ),
+    path("class_roles/", views.ClassRoleListView.as_view(), name="class_roles"),
+    path("class_roles/create/", views.ClassRoleCreateView.as_view(), name="create_class_role"),
+    path("class_roles/<int:pk>/edit/", views.ClassRoleEditView.as_view(), name="edit_class_role",),
+    path(
+        "class_roles/<int:pk>/delete/",
+        views.ClassRoleDeleteView.as_view(),
+        name="delete_class_role",
+    ),
 ]
diff --git a/aleksis/apps/alsijil/views.py b/aleksis/apps/alsijil/views.py
index c5616940d55cb3bfb577d9ae5a2b6bd7c30bb4bb..8803a8851ac27074753083d52230472dfd069982 100644
--- a/aleksis/apps/alsijil/views.py
+++ b/aleksis/apps/alsijil/views.py
@@ -27,6 +27,7 @@ from aleksis.core.util import messages
 from aleksis.core.util.core_helpers import get_site_preferences, objectgetter_optional
 
 from .forms import (
+    ClassRoleForm,
     ExcuseTypeForm,
     ExtraMarkForm,
     LessonDocumentationForm,
@@ -34,8 +35,8 @@ from .forms import (
     RegisterAbsenceForm,
     SelectForm,
 )
-from .models import ExcuseType, ExtraMark, LessonDocumentation, PersonalNote
-from .tables import ExcuseTypeTable, ExtraMarkTable
+from .models import ClassRole, ExcuseType, ExtraMark, LessonDocumentation, PersonalNote
+from .tables import ClassRoleTable, ExcuseTypeTable, ExtraMarkTable
 from .util.alsijil_helpers import get_lesson_period_by_pk, get_timetable_instance_by_pk
 
 
@@ -849,3 +850,47 @@ class ExcuseTypeDeleteView(PermissionRequiredMixin, RevisionMixin, AdvancedDelet
     template_name = "core/pages/delete.html"
     success_url = reverse_lazy("excuse_types")
     success_message = _("The excuse type has been deleted.")
+
+
+class ClassRoleListView(PermissionRequiredMixin, SingleTableView):
+    """Table of all class roles."""
+
+    model = ClassRole
+    table_class = ClassRoleTable
+    permission_required = "alsijil.view_classroles"
+    template_name = "alsijil/class_role/list.html"
+
+
+@method_decorator(never_cache, name="dispatch")
+class ClassRoleCreateView(PermissionRequiredMixin, AdvancedCreateView):
+    """Create view for class roles."""
+
+    model = ClassRole
+    form_class = ClassRoleForm
+    permission_required = "alsijil.add_classrole"
+    template_name = "alsijil/class_role/create.html"
+    success_url = reverse_lazy("class_roles")
+    success_message = _("The class role has been created.")
+
+
+@method_decorator(never_cache, name="dispatch")
+class ClassRoleEditView(PermissionRequiredMixin, AdvancedEditView):
+    """Edit view for class roles."""
+
+    model = ClassRole
+    form_class = ClassRoleForm
+    permission_required = "alsijil.edit_classrole"
+    template_name = "alsijil/class_role/edit.html"
+    success_url = reverse_lazy("class_roles")
+    success_message = _("The class role has been saved.")
+
+
+@method_decorator(never_cache, "dispatch")
+class ClassRoleDeleteView(PermissionRequiredMixin, RevisionMixin, AdvancedDeleteView):
+    """Delete view for class roles."""
+
+    model = ClassRole
+    permission_required = "alsijil.delete_classrole"
+    template_name = "core/pages/delete.html"
+    success_url = reverse_lazy("class_roles")
+    success_message = _("The class role has been deleted.")