diff --git a/aleksis/apps/alsijil/forms.py b/aleksis/apps/alsijil/forms.py
index 348b51bf85d2e52b631e33a1394e26b27d7a2bb5..8ea5ee1c553a3d40e82c3a015ed30e0206b7fd6a 100644
--- a/aleksis/apps/alsijil/forms.py
+++ b/aleksis/apps/alsijil/forms.py
@@ -6,20 +6,21 @@ from django.db.models import Count, Q
 from django.utils.translation import gettext_lazy as _
 
 from django_global_request.middleware import get_request
-from django_select2.forms import Select2Widget
+from django_select2.forms import ModelSelect2MultipleWidget, ModelSelect2Widget, Select2Widget
 from guardian.shortcuts import get_objects_for_user
 from material import Fieldset, Layout, Row
 
 from aleksis.apps.chronos.managers import TimetableType
 from aleksis.apps.chronos.models import TimePeriod
 from aleksis.core.models import Group, Person
+from aleksis.core.util.core_helpers import get_site_preferences
 from aleksis.core.util.predicates import check_global_permission
 
 from .models import (
-    GroupRole,
-    GroupRoleAssignment,
     ExcuseType,
     ExtraMark,
+    GroupRole,
+    GroupRoleAssignment,
     LessonDocumentation,
     PersonalNote,
 )
@@ -177,3 +178,76 @@ class GroupRoleForm(forms.ModelForm):
     class Meta:
         model = GroupRole
         fields = ["name", "icon", "colour"]
+
+
+class AssignGroupRoleForm(forms.ModelForm):
+    layout_base = ["groups", "person", "role", Row("date_start", "date_end")]
+
+    groups = forms.ModelMultipleChoiceField(
+        label=_("Group"),
+        required=True,
+        queryset=Group.objects.all(),
+        widget=ModelSelect2MultipleWidget(
+            model=Group,
+            search_fields=["name__icontains", "short_name__icontains"],
+            attrs={"data-minimum-input-length": 0, "class": "browser-default",},
+        ),
+    )
+    person = forms.ModelChoiceField(
+        label=_("Person"),
+        required=True,
+        queryset=Person.objects.all(),
+        widget=ModelSelect2Widget(
+            model=Person,
+            dependent_fields={"groups": "member_of"},
+            search_fields=[
+                "first_name__icontains",
+                "last_name__icontains",
+                "short_name__icontains",
+            ],
+            attrs={"data-minimum-input-length": 0, "class": "browser-default"},
+        ),
+    )
+
+    def __init__(self, request, *args, **kwargs):
+        self.request = request
+        initial = kwargs.get("initial", {})
+
+        # Build layout with or without groups field
+        base_layout = self.layout_base[:]
+        if "groups" in initial:
+            base_layout.remove("groups")
+        self.layout = Layout(*base_layout)
+
+        super().__init__(*args, **kwargs)
+
+        if "groups" in initial:
+            self.fields["groups"].required = False
+
+        # Filter persons by permissions
+        if not self.request.user.has_perm("alsijil.assign_grouprole"):  # Global permission
+            persons = Person.objects
+            if initial.get("groups"):
+                persons = persons.filter(member_of__in=initial["groups"])
+            if get_site_preferences()["alsijil__group_owners_can_assign_roles_to_parents"]:
+                persons = persons.filter(
+                    Q(member_of__owners=self.request.user.person)
+                    | Q(children__member_of__owners=self.request.user.person)
+                )
+            else:
+                persons = persons.filter(member_of__owners=self.request.user.person)
+            self.fields["person"].queryset = persons
+
+    def clean_groups(self):
+        """Ensure that only permitted groups are used."""
+        return self.initial["groups"] if "groups" in self.initial else self.cleaned_data["groups"]
+
+    class Meta:
+        model = GroupRoleAssignment
+        fields = ["groups", "person", "role", "date_start", "date_end"]
+
+
+class GroupRoleAssignmentEditForm(forms.ModelForm):
+    class Meta:
+        model = GroupRoleAssignment
+        fields = ["date_start", "date_end"]
diff --git a/aleksis/apps/alsijil/models.py b/aleksis/apps/alsijil/models.py
index 440e12c5715f96f22e162c71632b1bdb6580cc3a..8890c0d286110a06f3b651c51e886157270db36d 100644
--- a/aleksis/apps/alsijil/models.py
+++ b/aleksis/apps/alsijil/models.py
@@ -13,6 +13,7 @@ from aleksis.apps.alsijil.data_checks import (
     PersonalNoteOnHolidaysDataCheck,
 )
 from aleksis.apps.alsijil.managers import PersonalNoteManager
+from aleksis.apps.chronos.managers import GroupPropertiesMixin
 from aleksis.apps.chronos.mixins import WeekRelatedMixin
 from aleksis.apps.chronos.models import LessonPeriod
 from aleksis.apps.chronos.util.date import get_current_year
@@ -246,7 +247,7 @@ class GroupRole(ExtensibleModel):
         verbose_name_plural = _("Group roles")
 
 
-class GroupRoleAssignment(ExtensibleModel):
+class GroupRoleAssignment(GroupPropertiesMixin, ExtensibleModel):
     role = models.ForeignKey(
         GroupRole,
         on_delete=models.CASCADE,
@@ -260,9 +261,7 @@ class GroupRoleAssignment(ExtensibleModel):
         verbose_name=_("Assigned person"),
     )
     groups = models.ManyToManyField(
-        "core.Group",
-        related_name="group_roles",
-        verbose_name=_("Groups"),
+        "core.Group", related_name="group_roles", verbose_name=_("Groups"),
     )
     date_start = models.DateField(verbose_name=_("Start date"))
     date_end = models.DateField(
diff --git a/aleksis/apps/alsijil/preferences.py b/aleksis/apps/alsijil/preferences.py
index 0e9706da1e41cdddbdcc9191b353abcb891f2fc6..010c1a6961053e2c5510bf83182a0e8cc30f5a09 100644
--- a/aleksis/apps/alsijil/preferences.py
+++ b/aleksis/apps/alsijil/preferences.py
@@ -74,3 +74,13 @@ class ActivateGroupRoles(BooleanPreference):
     name = "activate_group_roles"
     default = True
     verbose_name = _("Activate support for creating and assigning group roles")
+
+
+@site_preferences_registry.register
+class GroupOwnersCanAssignRolesToParents(BooleanPreference):
+    section = alsijil
+    name = "group_owners_can_assign_roles_to_parents"
+    default = False
+    verbose_name = _(
+        "Allow group owners to assign group roles to the parents of the group's members"
+    )
diff --git a/aleksis/apps/alsijil/rules.py b/aleksis/apps/alsijil/rules.py
index 0273c2c748418dc326dfb8c82c9ad48642eae046..3b4d93f0ff69cf1d08084838aac339cfc36f4c1e 100644
--- a/aleksis/apps/alsijil/rules.py
+++ b/aleksis/apps/alsijil/rules.py
@@ -14,6 +14,7 @@ from .util.predicates import (
     has_personal_note_group_perm,
     is_group_member,
     is_group_owner,
+    is_group_role_assignment_group_owner,
     is_lesson_parent_group_owner,
     is_lesson_participant,
     is_lesson_teacher,
@@ -219,12 +220,13 @@ add_perm("alsijil.edit_extramark", edit_extramark_predicate)
 delete_extramark_predicate = view_extramarks_predicate & has_global_perm("alsijil.delete_extramark")
 add_perm("alsijil.delete_extramark", delete_extramark_predicate)
 
+group_roles_activated_predicate = has_person & is_site_preference_set(
+    "alsijil", "activate_group_roles"
+)
 
 # View group role list
-view_group_roles_predicate = (
-    has_person
-    & is_site_preference_set("alsijil", "activate_group_roles")
-    & has_global_perm("alsijil.view_grouprole")
+view_group_roles_predicate = group_roles_activated_predicate & has_global_perm(
+    "alsijil.view_grouprole"
 )
 add_perm("alsijil.view_grouproles", view_group_roles_predicate)
 
@@ -241,3 +243,31 @@ delete_group_role_predicate = view_group_roles_predicate & has_global_perm(
     "alsijil.delete_grouprole"
 )
 add_perm("alsijil.delete_grouprole", delete_group_role_predicate)
+
+view_assigned_group_roles_predicate = group_roles_activated_predicate & (
+    is_group_owner
+    | has_global_perm("alsjil.assign_grouprole")
+    | has_object_perm("alsijil.assign_grouprole")
+)
+add_perm("alsijil.view_assigned_grouproles", view_assigned_group_roles_predicate)
+
+assign_group_role_person_predicate = group_roles_activated_predicate & (
+    is_person_group_owner | has_global_perm("alsjil.assign_grouprole")
+)
+add_perm("alsijil.assign_grouprole_to_person", assign_group_role_person_predicate)
+
+assign_group_role_group_predicate = view_assigned_group_roles_predicate
+add_perm("alsijil.assign_grouprole_for_group", assign_group_role_group_predicate)
+
+edit_group_role_assignment_predicate = group_roles_activated_predicate & (
+    has_global_perm("alsjil.assign_grouprole") | is_group_role_assignment_group_owner
+)
+add_perm("alsijil.edit_grouproleassignment", edit_group_role_assignment_predicate)
+
+stop_group_role_assignment_predicate = edit_group_role_assignment_predicate
+add_perm("alsijil.stop_grouproleassignment", stop_group_role_assignment_predicate)
+
+delete_group_role_assignment_predicate = group_roles_activated_predicate & (
+    has_global_perm("alsjil.assign_grouprole") | is_group_role_assignment_group_owner
+)
+add_perm("alsijil.delete_grouproleassignment", delete_group_role_assignment_predicate)
diff --git a/aleksis/apps/alsijil/templates/alsijil/class_register/groups.html b/aleksis/apps/alsijil/templates/alsijil/class_register/groups.html
index a2bf6799df9a4f9390fd1a912d5eb9dab51c3cef..f300f3b8457eed468cb3bad31821eb8d94033cf8 100644
--- a/aleksis/apps/alsijil/templates/alsijil/class_register/groups.html
+++ b/aleksis/apps/alsijil/templates/alsijil/class_register/groups.html
@@ -1,6 +1,6 @@
 {# -*- engine:django -*- #}
 {% extends "core/base.html" %}
-{% load i18n static %}
+{% load i18n static rules %}
 
 {% block browser_title %}{% blocktrans %}My groups{% endblocktrans %}{% endblock %}
 
@@ -38,6 +38,13 @@
               <i class="material-icons left">view_week</i>
               {% trans "Week view" %}
             </a>
+            {% has_perm "alsijil.view_assigned_grouproles" user group as can_view_assigned_group_roles %}
+            {% if can_view_assigned_group_roles %}
+              <a class="btn primary waves-effect waves-light" href="{% url 'assigned_group_roles' group.pk %}">
+                <i class="material-icons left">assignment_ind</i>
+                {% trans "Roles" %}
+              </a>
+            {% endif %}
             <a class="btn primary waves-effect waves-light" href="{% url "full_register_group" group.pk %}"
                target="_blank">
               <i class="material-icons left">print</i>
@@ -75,6 +82,15 @@
               {% trans "Week view" %}
             </a>
           </p>
+          {% has_perm "alsijil.view_assigned_grouproles" user group as can_view_assigned_group_roles %}
+          {% if can_view_assigned_group_roles %}
+            <p>
+              <a class="btn primary waves-effect waves-light" href="{% url 'assigned_group_roles' group.pk %}">
+                <i class="material-icons left">assignment_ind</i>
+                {% trans "Roles" %}
+              </a>
+            </p>
+          {% endif %}
           <p>
             <a class="btn primary waves-effect waves-light" href="{% url "full_register_group" group.pk %}"
                target="_blank">
diff --git a/aleksis/apps/alsijil/templates/alsijil/group_role/assign.html b/aleksis/apps/alsijil/templates/alsijil/group_role/assign.html
new file mode 100644
index 0000000000000000000000000000000000000000..3405e8da56700ab9e29574c8d6f2d0a0a4b5e09b
--- /dev/null
+++ b/aleksis/apps/alsijil/templates/alsijil/group_role/assign.html
@@ -0,0 +1,33 @@
+{# -*- engine:django -*- #}
+
+{% extends "core/base.html" %}
+
+{% load i18n rules any_js material_form %}
+
+{% block browser_title %}
+  {% blocktrans with group=group.name %}Assign group role for {{ group }}{% endblocktrans %}
+{% endblock %}
+{% block page_title %}
+  {% blocktrans with group=group.name %}Assign group role for {{ group }}{% endblocktrans %}
+{% endblock %}
+
+{% block extra_head %}
+  {{ form.media.css }}
+  {% include_css "select2-materialize" %}
+{% endblock %}
+
+{% block content %}
+  <form action="" method="post">
+    {% csrf_token %}
+    {% form form=form %}{% endform %}
+
+    <button type="submit" class="btn green waves-effect waves-light">
+      <i class="material-icons left">assignment_ind</i>
+      {% trans "Assign" %}
+    </button>
+  </form>
+
+  {% include_js "select2-materialize" %}
+  {{ form.media.js }}
+{% endblock %}
+
diff --git a/aleksis/apps/alsijil/templates/alsijil/group_role/assigned_list.html b/aleksis/apps/alsijil/templates/alsijil/group_role/assigned_list.html
new file mode 100644
index 0000000000000000000000000000000000000000..123bc4c2e23d37f67599a3c7a7cc49bd9bae14f9
--- /dev/null
+++ b/aleksis/apps/alsijil/templates/alsijil/group_role/assigned_list.html
@@ -0,0 +1,144 @@
+{# -*- engine:django -*- #}
+
+{% extends "core/base.html" %}
+
+{% load i18n rules any_js material_form static %}
+{% load render_table from django_tables2 %}
+
+{% block browser_title %}
+  {% blocktrans with group=object.name %}Group roles for {{ group }}{% endblocktrans %}
+{% endblock %}
+{% block page_title %}
+  {% blocktrans with group=object.name %}Group roles for {{ group }}{% endblocktrans %}
+{% endblock %}
+
+{% block extra_head %}
+  {{ block.super }}
+  <link rel="stylesheet" href="{% static "css/alsijil/alsijil.css" %}"/>
+{% endblock %}
+
+{% block content %}
+  {% url "assigned_group_roles" object.pk as back_url %}
+
+  <p>
+    {% has_perm "alsijil.view_my_groups" user as can_view_group_overview %}
+    {% if can_view_group_overview %}
+      <a class="btn waves-effect waves-light" href="{% url "my_groups" %}">
+        <i class="material-icons left">arrow_back</i>
+        {% trans "Back to my groups" %}
+      </a>
+    {% endif %}
+
+    {% has_perm "alsijil.assign_grouprole_for_group" user object as can_assign_group_role %}
+    {% if can_assign_group_role %}
+      <a class="btn green waves-effect waves-light" href="{% url "assign_group_role" object.pk %}">
+        <i class="material-icons left">assignment_ind</i>
+        {% trans "Assign a role to a person" %}
+      </a>
+    {% endif %}
+  </p>
+
+  <div class="row">
+    <div class="col s12">
+      <ul class="tabs">
+        <li class="tab">
+          <a class="active" href="#current">{% trans "Current roles" %} ({{ today|date:"SHORT_DATE_FORMAT" }})</a>
+        </li>
+        <li class="tab">
+          <a href="#all">{% trans "All assignments" %}</a>
+        </li>
+      </ul>
+    </div>
+
+    <div id="current" class="col s12">
+      <div class="collection">
+        {% for role in roles %}
+          <div class="collection-item">
+            <div class="row no-margin">
+              <div class="col s12 m5 l4 xl3 no-padding">
+                {% if can_assign_group_role %}
+                  <a class="btn waves-effect waves-light right hide-on-med-and-up"
+                     href="{% url "assign_group_role" object.pk role.pk %}">
+                    <i class="material-icons center">add</i>
+                  </a>
+                {% endif %}
+
+                <div class="btn-margin">
+                  {% include "alsijil/group_role/chip.html" with role=role %}
+                </div>
+              </div>
+
+              <div class="col s12 m7 l8 xl9 no-padding">
+                {% if can_assign_group_role %}
+                  <a class="btn waves-effect waves-light right hide-on-small-only"
+                     href="{% url "assign_group_role" object.pk role.pk %}">
+                    <i class="material-icons center">add</i>
+                  </a>
+                {% endif %}
+
+                {% for assignment in role.assignments.all %}
+                  <a class="chip dropdown-trigger" href="#"
+                     data-target="dropdown-{{ assignment.pk }}" title="{{ assignment }}">{{ assignment.person }}
+                    {% if object not in assignment.groups.all %}
+                      <small>({{ assignment.group_names }})</small>
+                    {% endif %}
+                  </a>
+
+                  {% include "alsijil/group_role/assignment_options.html" with assignment=assignment back_url=back_url %}
+                  {% empty %}
+                  <div class="grey-text darken-3">{% trans "No one assigned." %}</div>
+                {% endfor %}
+              </div>
+            </div>
+          </div>
+        {% endfor %}
+      </div>
+
+      <div class="alert primary">
+        <div>
+          <i class="material-icons left">info</i>
+          {% blocktrans %}
+            You can get some additional actions for each group role assignment if you click on the name of the
+            corresponding person.
+          {% endblocktrans %}
+        </div>
+      </div>
+    </div>
+
+
+    <div class="col s12 " id="all">
+      <table class="responsive-table">
+        <thead>
+        <tr>
+          <th class="chip-height">{% trans "Group role" %}</th>
+          <th>{% trans "Person" %}</th>
+          <th>{% trans "Start date" %}</th>
+          <th>{% trans "End date" %}</th>
+          <th>{% trans "Actions" %}</th>
+        </tr>
+        </thead>
+        {% for assignment in assignments %}
+          <tr>
+            <td>
+              {% include "alsijil/group_role/chip.html" with role=assignment.role %}
+            </td>
+            <td>
+              {{ assignment.person }}
+            </td>
+            <td>{{ assignment.date_start }}</td>
+            <td>{{ assignment.date_end|default:"–" }}</td>
+            <td>
+              <a class="btn waves-effect waves-light dropdown-trigger" href="#"
+                 data-target="dropdown-{{ assignment.pk }}-d2">
+                <i class="material-icons left">list</i>
+                {% trans "Actions" %}
+              </a>
+              {% include "alsijil/group_role/assignment_options.html" with assignment=assignment back_url=back_url suffix="-d2" %}
+            </td>
+          </tr>
+        {% endfor %}
+      </table>
+    </div>
+  </div>
+{% endblock %}
+
diff --git a/aleksis/apps/alsijil/templates/alsijil/group_role/assignment_options.html b/aleksis/apps/alsijil/templates/alsijil/group_role/assignment_options.html
new file mode 100644
index 0000000000000000000000000000000000000000..ff50720db9dacd5437fb4b43ecf19e9719138de3
--- /dev/null
+++ b/aleksis/apps/alsijil/templates/alsijil/group_role/assignment_options.html
@@ -0,0 +1,33 @@
+{# -*- engine:django -*- #}
+
+{% load i18n rules %}
+
+{% has_perm "alsijil.edit_grouproleassignment" user assignment as can_edit %}
+{% has_perm "alsijil.stop_grouproleassignment" user assignment as can_stop %}
+{% has_perm "alsijil.delete_grouproleassignment" user assignment as can_delete %}
+
+<ul id="dropdown-{{ assignment.pk }}{{ suffix }}" class="dropdown-content">
+  {% if can_edit %}
+    <li>
+      <a href="{% url "edit_group_role_assignment" assignment.pk %}?next={{ back_url }}">
+        <i class="material-icons left">edit</i> {% trans "Edit" %}
+      </a>
+    </li>
+  {% endif %}
+
+  {% if not assignment.date_end and can_stop %}
+    <li>
+      <a href="#">
+        <i class="material-icons left">stop</i> {% trans "Stop" %}
+      </a>
+    </li>
+  {% endif %}
+
+  {% if can_delete %}
+    <li>
+      <a href="{% url "delete_group_role_assignment" assignment.pk %}?next={{ back_url }}" class="red-text">
+        <i class="material-icons left">delete</i> {% trans "Delete" %}
+      </a>
+    </li>
+  {% endif %}
+</ul>
\ No newline at end of file
diff --git a/aleksis/apps/alsijil/templates/alsijil/group_role/chip.html b/aleksis/apps/alsijil/templates/alsijil/group_role/chip.html
new file mode 100644
index 0000000000000000000000000000000000000000..f50fc10e965c92762451a985dc30c28d78c64b08
--- /dev/null
+++ b/aleksis/apps/alsijil/templates/alsijil/group_role/chip.html
@@ -0,0 +1,6 @@
+{# -*- engine:django -*- #}
+
+<div class="chip white-text {{ role.colour|default:"black" }}">
+  <i class="material-icons left">{{ role.icon|default:"assignment_ind" }}</i>
+  {{ role.name }}
+</div>
\ No newline at end of file
diff --git a/aleksis/apps/alsijil/templates/alsijil/group_role/edit_assignment.html b/aleksis/apps/alsijil/templates/alsijil/group_role/edit_assignment.html
new file mode 100644
index 0000000000000000000000000000000000000000..bc5038654980374fcd0c65639b7c4d3488871625
--- /dev/null
+++ b/aleksis/apps/alsijil/templates/alsijil/group_role/edit_assignment.html
@@ -0,0 +1,18 @@
+{# -*- engine:django -*- #}
+
+{% extends "core/base.html" %}
+
+{% load i18n rules material_form %}
+
+{% block browser_title %}{% blocktrans %}Edit group role assignment{% endblocktrans %}{% endblock %}
+{% block page_title %}{% blocktrans %}Edit group role assignment{% endblocktrans %}{% endblock %}
+
+{% block content %}
+  <form action="" method="post">
+    {% csrf_token %}
+    {% form form=form %}{% endform %}
+
+    {% include "core/partials/save_button.html" %}
+  </form>
+{% endblock %}
+
diff --git a/aleksis/apps/alsijil/urls.py b/aleksis/apps/alsijil/urls.py
index cb9b45dac388203ac75b170aba1a5d6c2a757fd0..0e007075d04cb15e0d4fd649b896225f12beced2 100644
--- a/aleksis/apps/alsijil/urls.py
+++ b/aleksis/apps/alsijil/urls.py
@@ -57,4 +57,34 @@ urlpatterns = [
         views.GroupRoleDeleteView.as_view(),
         name="delete_group_role",
     ),
+    path(
+        "groups/<int:pk>/group_roles/",
+        views.AssignedGroupRolesView.as_view(),
+        name="assigned_group_roles",
+    ),
+    path(
+        "groups/<int:pk>/group_roles/assign/",
+        views.AssignGroupRoleView.as_view(),
+        name="assign_group_role",
+    ),
+    path(
+        "groups/<int:pk>/group_roles/<int:role_pk>/assign/",
+        views.AssignGroupRoleView.as_view(),
+        name="assign_group_role",
+    ),
+    path(
+        "group_roles/assignments/<int:pk>/edit/",
+        views.GroupRoleAssignmentEditView.as_view(),
+        name="edit_group_role_assignment",
+    ),
+    path(
+        "group_roles/assignments/<int:pk>/stop/",
+        views.GroupRoleAssignmentStopView.as_view(),
+        name="stop_group_role_assignment",
+    ),
+    path(
+        "group_roles/assignments/<int:pk>/delete/",
+        views.GroupRoleAssignmentDeleteView.as_view(),
+        name="delete_group_role_assignment",
+    ),
 ]
diff --git a/aleksis/apps/alsijil/util/predicates.py b/aleksis/apps/alsijil/util/predicates.py
index 95c7825c2a1eceeb2aed222260816ec9575ad0a8..c1d6d063e7361e05468a8588efd65a7474e753c3 100644
--- a/aleksis/apps/alsijil/util/predicates.py
+++ b/aleksis/apps/alsijil/util/predicates.py
@@ -246,3 +246,18 @@ def is_personal_note_lesson_parent_group_owner(user: User, obj: PersonalNote) ->
 def is_teacher(user: User, obj: Person) -> bool:
     """Predicate which checks if the provided object is a teacher."""
     return user.person.is_teacher
+
+
+@predicate
+def is_group_role_assignment_group_owner(user: User, obj: Union[Group, Person]) -> bool:
+    """Predicate for group owners of a group role assignment.
+
+    Checks whether the person linked to the user is the owner of the groups
+    linked to the given group role assignment.
+    If there isn't provided a group role assignment, it will return `False`.
+    """
+    if obj:
+        for group in obj.groups.all():
+            if user.person in list(group.owners.all()):
+                return True
+    return False
diff --git a/aleksis/apps/alsijil/views.py b/aleksis/apps/alsijil/views.py
index dac92a9f9e98db321156539e36951fe701e681db..b37becc357e78adcc680bcd4099594870973a9d7 100644
--- a/aleksis/apps/alsijil/views.py
+++ b/aleksis/apps/alsijil/views.py
@@ -6,6 +6,7 @@ from django.db.models import Count, Exists, OuterRef, Prefetch, Q, Subquery, Sum
 from django.http import Http404, HttpRequest, HttpResponse, HttpResponseNotFound
 from django.shortcuts import get_object_or_404, redirect, render
 from django.urls import reverse, reverse_lazy
+from django.utils import timezone
 from django.utils.decorators import method_decorator
 from django.utils.translation import ugettext as _
 from django.views.decorators.cache import never_cache
@@ -21,30 +22,36 @@ from aleksis.apps.chronos.managers import TimetableType
 from aleksis.apps.chronos.models import 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.mixins import AdvancedCreateView, AdvancedDeleteView, AdvancedEditView
+from aleksis.core.mixins import (
+    AdvancedCreateView,
+    AdvancedDeleteView,
+    AdvancedEditView,
+    SuccessNextMixin,
+)
 from aleksis.core.models import Group, Person, SchoolTerm
 from aleksis.core.util import messages
 from aleksis.core.util.core_helpers import get_site_preferences, objectgetter_optional
 
 from .forms import (
     AssignGroupRoleForm,
-    GroupRoleForm,
     ExcuseTypeForm,
     ExtraMarkForm,
+    GroupRoleAssignmentEditForm,
+    GroupRoleForm,
     LessonDocumentationForm,
     PersonalNoteFormSet,
     RegisterAbsenceForm,
     SelectForm,
 )
 from .models import (
-    GroupRole,
-    GroupRoleAssignment,
     ExcuseType,
     ExtraMark,
+    GroupRole,
+    GroupRoleAssignment,
     LessonDocumentation,
     PersonalNote,
 )
-from .tables import GroupRoleTable, ExcuseTypeTable, ExtraMarkTable
+from .tables import ExcuseTypeTable, ExtraMarkTable, GroupRoleTable
 from .util.alsijil_helpers import get_lesson_period_by_pk, get_timetable_instance_by_pk
 
 
@@ -902,3 +909,117 @@ class GroupRoleDeleteView(PermissionRequiredMixin, RevisionMixin, AdvancedDelete
     template_name = "core/pages/delete.html"
     success_url = reverse_lazy("group_roles")
     success_message = _("The group role has been deleted.")
+
+
+class AssignedGroupRolesView(PermissionRequiredMixin, DetailView):
+    permission_required = "alsijil.view_assigned_grouproles"
+    model = Group
+    template_name = "alsijil/group_role/assigned_list.html"
+
+    def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
+        context = super().get_context_data()
+
+        today = timezone.now().date()
+        context["today"] = today
+
+        self.roles = GroupRole.objects.prefetch_related(
+            Prefetch(
+                "assignments",
+                queryset=GroupRoleAssignment.objects.filter(
+                    Q(date_start__lte=today) & (Q(date_end__gte=today) | Q(date_end__isnull=True))
+                )
+                .filter(Q(groups=self.object) | Q(groups__child_groups=self.object))
+                .distinct(),
+            )
+        )
+        context["roles"] = self.roles
+        assignments = (
+            GroupRoleAssignment.objects.filter(
+                Q(groups=self.object) | Q(groups__child_groups=self.object)
+            )
+            .distinct()
+            .order_by("-date_start")
+        )
+        context["assignments"] = assignments
+        return context
+
+
+@method_decorator(never_cache, name="dispatch")
+class AssignGroupRoleView(PermissionRequiredMixin, AdvancedCreateView):
+    model = GroupRoleAssignment
+    form_class = AssignGroupRoleForm
+    permission_required = "alsijil.assign_grouprole_for_group"
+    template_name = "alsijil/group_role/assign.html"
+    success_message = _("The group role has been assigned.")
+
+    def get_success_url(self) -> str:
+        return reverse("assigned_group_roles", args=[self.group.pk])
+
+    def get_permission_object(self):
+        self.group = get_object_or_404(Group, pk=self.kwargs.get("pk"))
+        try:
+            self.role = GroupRole.objects.get(pk=self.kwargs.get("role_pk"))
+        except GroupRole.DoesNotExist:
+            self.role = None
+        return self.group
+
+    def get_form_kwargs(self):
+        kwargs = super().get_form_kwargs()
+        kwargs["request"] = self.request
+        kwargs["initial"] = {"role": self.role, "groups": [self.group]}
+        return kwargs
+
+    def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
+        context = super().get_context_data(**kwargs)
+        context["role"] = self.role
+        context["group"] = self.group
+        return context
+
+
+@method_decorator(never_cache, name="dispatch")
+class GroupRoleAssignmentEditView(PermissionRequiredMixin, SuccessNextMixin, AdvancedEditView):
+    """Edit view for group role assignments."""
+
+    model = GroupRoleAssignment
+    form_class = GroupRoleAssignmentEditForm
+    permission_required = "alsijil.edit_grouproleassignment"
+    template_name = "alsijil/group_role/edit_assignment.html"
+    success_message = _("The group role assignment has been saved.")
+
+    def get_default_success_url(self) -> str:
+        pk = self.object.groups.first().pk
+        return reverse("assigned_group_roles", args=[pk])
+
+
+@method_decorator(never_cache, "dispatch")
+class GroupRoleAssignmentStopView(PermissionRequiredMixin, SuccessNextMixin, DetailView):
+    model = GroupRoleAssignment
+    permission_required = "alsijil.stop_grouproleassignment"
+
+    def get_default_success_url(self) -> str:
+        pk = self.object.groups.first().pk
+        return reverse("assigned_group_roles", args=[pk])
+
+    def get(self, request, *args, **kwargs):
+        self.object = self.get_object()
+        if not self.object.date_end:
+            self.object.date_end = timezone.now().date()
+            self.object.save()
+            messages.success(request, _("The group role assignment has been stopped."))
+        return redirect(self.get_success_url())
+
+
+@method_decorator(never_cache, "dispatch")
+class GroupRoleAssignmentDeleteView(
+    PermissionRequiredMixin, RevisionMixin, SuccessNextMixin, AdvancedDeleteView
+):
+    """Delete view for group role assignments."""
+
+    model = GroupRoleAssignment
+    permission_required = "alsijil.delete_grouproleassignment"
+    template_name = "core/pages/delete.html"
+    success_message = _("The group role assignment has been deleted.")
+
+    def get_default_success_url(self) -> str:
+        pk = self.object.groups.first().pk
+        return reverse("assigned_group_roles", args=[pk])