We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Improve access denied messages and 403 page
Improve access denied messages and make them somewhat more explanatory, and also improve the 403 page so it's not quite so aggressive towards would-be learners.
See [Juha's comment](https://github.com/apluslms/a-plus/pull/853#discussion_r692139980) on PR #853 
Improve access denied messages and 403 page
Fixes #858

# Description

**What?**

Improve access denied message and 403 page

**Why?**

To make it more clear why user may not access certain pages

**How?**

By adding new error texts and changing how the 403 page looks like so the user can more easily navigate a-plus in case they get to the 403 page.

Fixes #858 


# Testing

**Remember to add or update unit tests for new features and changes.**

* How to [test your changes in A-plus](https://github.com/apluslms/a-plus/tree/master/doc#running-tests-and-updating-translations)
* How to [test accessibility](https://wiki.aalto.fi/display/EDIT/How+to+check+the+accessibility+of+pull+requests)


**What type of test did you run?**

- [ ] Accessibility test using the [WAVE](https://wave.webaim.org/extension/) extension.
- [ ] Django unit tests.
- [ ] Selenium tests.
- [ ] Other test. *(Add a description below)*
- [x] Manual testing.

[ADD A DESCRIPTION ABOUT WHAT YOU TESTED MANUALLY]

**Did you test the changes in**

- [ ] Chrome
- [x] Firefox
- [ ] This pull request cannot be tested in the browser.

**Think of what is affected by these changes and could become broken**

# Translation

- [x] Did you modify or add new strings in the user interface? ([Read about how to create translation](https://github.com/apluslms/a-plus/tree/master/doc#running-tests-and-updating-translations))

# Programming style

- [ ] Did you follow our [style guides](https://apluslms.github.io/contribute/styleguides/)?
- [ ] Did you use Python type hinting in all functions that you added or edited? ([type hints](https://docs.python.org/3/library/typing.html) for function parameters and return values)

# Have you updated the README or other relevant documentation?

- [ ] documents inside the doc directory.
- [ ] README.md.
- [ ] Aplus Manual.
- [ ] Other documentation (mention below which documentation).

# Is it Done?

- [ ] Reviewer has finished the code review
- [ ] After the review, the developer has made changes accordingly
- [ ] Customer/Teacher has accepted the implementation of the feature

*Clean up your git commit history before submitting the pull request!*


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `authorization/permissions.py`
Content:
```
1 """
2 Base permission classes.
3 
4 These classes use same interface than ones in django-rest-framework and
5 are usable with APIViews too.
6 """
7 import string
8 
9 from django.utils.text import format_lazy
10 from django.utils.translation import gettext_lazy as _
11 from rest_framework.permissions import BasePermission as Permission
12 
13 from lib.helpers import Enum
14 
15 
16 SAFE_METHODS = ('GET', 'HEAD', 'OPTIONS')
17 
18 
19 class FilterBackend:
20     """
21     FilterBackend interface
22     """
23     def filter_queryset(self, request, queryset, view):
24         """
25         Return a filtered queryset.
26         """
27         raise NotImplementedError
28 
29     def get_fields(self, view): # pylint: disable=unused-argument
30         return []
31 
32 
33 class NoPermission(Permission):
34     """
35     Base Permission class that gives no access permission to anyone.
36     """
37     def has_permission(self, request, view):
38         return False
39 
40     def has_object_permission(self, request, view, obj):
41         return False
42 
43 
44 class MessageMixin:
45     """
46     Adds easy way to specify what exactly caused the PermissionDenied
47     """
48     def error_msg(self, message: str, delim=None, format=None, replace=False): # pylint: disable=redefined-builtin
49         """
50         Add extra text to self.message about the reason why permission
51         was denied. Uses lazy object so the message string is evaluated
52         only when rendered.
53 
54         If optional argument `format` is given, then it's used with format_lazy
55         to format the message with the dictionary arguments from `format` arg.
56 
57         Optional argument `delim` can be used to change the string used to join
58         self.message and `message`.
59 
60         If optional argument `replace` is true, then self.message is replaced with
61         the `message`.
62         """
63         if delim is None:
64             delim = ': '
65 
66         if format:
67             message = format_lazy(message, **format)
68 
69         if replace:
70             self.message = message
71         else:
72             assert 'message' not in self.__dict__, (
73                 "You are calling error_msg without replace=True "
74                 "after calling it with it first. Fix your code by removing "
75                 "the first method call and add replace=True to the second method call too."
76             )
77             msg_without_end_punctuation = (
78                 self.message[0:-1] if self.message[-1] in string.punctuation
79                 else self.message
80             )
81             self.message = format_lazy(
82                 '{}{}{}',
83                 msg_without_end_punctuation,
84                 delim,
85                 message,
86             )
87 
88 
89 # Access mode
90 # ===========
91 
92 # All access levels
93 ACCESS = Enum(
94     ('ANONYMOUS', 0, _('ACCESS_ANYONE')),
95     ('ENROLL', 1, None),
96     ('STUDENT', 3, _('ACCESS_ANY_STUDENT')),
97     ('ENROLLED', 4, _('ACCESS_ENROLLED_STUDENT')),
98     ('ASSISTANT', 5, _('ACCESS_COURSE_ASSISTANT')),
99     ('GRADING', 6, _('ACCESS_GRADING')),
100     ('TEACHER', 10, _('ACCESS_TEACHER')),
101     ('SUPERUSER', 100, _('ACCESS_SUPERUSER')),
102 )
103 
104 
105 class AccessModePermission(MessageMixin, Permission):
106     """
107     If view has access_mode that is not anonymous, then require authentication
108     """
109     message = _('ACCESS_PERMISSION_DENIED_MSG')
110 
111     def has_permission(self, request, view):
112         access_mode = view.get_access_mode()
113 
114         if access_mode == ACCESS.ANONYMOUS:
115             return True
116         if not request.user.is_authenticated:
117             return False
118 
119         if access_mode >= ACCESS.SUPERUSER:
120             return request.user.is_superuser
121 
122         if access_mode >= ACCESS.TEACHER:
123             if not view.is_teacher:
124                 self.error_msg(_('ACCESS_ERROR_ONLY_TEACHERS'))
125                 return False
126 
127         elif access_mode >= ACCESS.ASSISTANT:
128             if not view.is_course_staff:
129                 self.error_msg(_('ACCESS_ERROR_ONLY_COURSE_STAFF'))
130                 return False
131 
132         elif access_mode == ACCESS.ENROLLED:
133             if not view.is_course_staff and not view.is_student:
134                 self.error_msg(_('ACCESS_ERROR_ONLY_ENROLLED_STUDENTS'))
135                 return False
136 
137         return True
138 
139 
140 # Object permissions
141 # ==================
142 
143 
144 class ObjectVisibleBasePermission(MessageMixin, Permission):
145     model = None
146     obj_var = None
147 
148     def has_permission(self, request, view):
149         obj = getattr(view, self.obj_var, None)
150         return (
151             obj is None or
152             self.has_object_permission(request, view, obj)
153         )
154 
155     def has_object_permission(self, request, view, obj):
156         user = request.user
157         return (
158             # skip objects that are not the model in question
159             not isinstance(obj, self.model) or # pylint: disable=isinstance-second-argument-not-valid-type
160             user.is_staff or
161             user.is_superuser or
162             self.is_object_visible(request, view, obj)
163         )
164 
165     def is_object_visible(self, request, view, obj):
166         raise NotImplementedError
167
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/authorization/permissions.py b/authorization/permissions.py
--- a/authorization/permissions.py
+++ b/authorization/permissions.py
@@ -114,6 +114,7 @@
         if access_mode == ACCESS.ANONYMOUS:
             return True
         if not request.user.is_authenticated:
+            self.error_msg(_('ACCESS_ERROR_ONLY_AUTHENTICATED'))
             return False
 
         if access_mode >= ACCESS.SUPERUSER:

{"golden_diff": "diff --git a/authorization/permissions.py b/authorization/permissions.py\n--- a/authorization/permissions.py\n+++ b/authorization/permissions.py\n@@ -114,6 +114,7 @@\n         if access_mode == ACCESS.ANONYMOUS:\n             return True\n         if not request.user.is_authenticated:\n+            self.error_msg(_('ACCESS_ERROR_ONLY_AUTHENTICATED'))\n             return False\n \n         if access_mode >= ACCESS.SUPERUSER:\n", "issue": "Improve access denied messages and 403 page\nImprove access denied messages and make them somewhat more explanatory, and also improve the 403 page so it's not quite so aggressive towards would-be learners.\r\nSee [Juha's comment](https://github.com/apluslms/a-plus/pull/853#discussion_r692139980) on PR #853 \nImprove access denied messages and 403 page\nFixes #858\r\n\r\n# Description\r\n\r\n**What?**\r\n\r\nImprove access denied message and 403 page\r\n\r\n**Why?**\r\n\r\nTo make it more clear why user may not access certain pages\r\n\r\n**How?**\r\n\r\nBy adding new error texts and changing how the 403 page looks like so the user can more easily navigate a-plus in case they get to the 403 page.\r\n\r\nFixes #858 \r\n\r\n\r\n# Testing\r\n\r\n**Remember to add or update unit tests for new features and changes.**\r\n\r\n* How to [test your changes in A-plus](https://github.com/apluslms/a-plus/tree/master/doc#running-tests-and-updating-translations)\r\n* How to [test accessibility](https://wiki.aalto.fi/display/EDIT/How+to+check+the+accessibility+of+pull+requests)\r\n\r\n\r\n**What type of test did you run?**\r\n\r\n- [ ] Accessibility test using the [WAVE](https://wave.webaim.org/extension/) extension.\r\n- [ ] Django unit tests.\r\n- [ ] Selenium tests.\r\n- [ ] Other test. *(Add a description below)*\r\n- [x] Manual testing.\r\n\r\n[ADD A DESCRIPTION ABOUT WHAT YOU TESTED MANUALLY]\r\n\r\n**Did you test the changes in**\r\n\r\n- [ ] Chrome\r\n- [x] Firefox\r\n- [ ] This pull request cannot be tested in the browser.\r\n\r\n**Think of what is affected by these changes and could become broken**\r\n\r\n# Translation\r\n\r\n- [x] Did you modify or add new strings in the user interface? ([Read about how to create translation](https://github.com/apluslms/a-plus/tree/master/doc#running-tests-and-updating-translations))\r\n\r\n# Programming style\r\n\r\n- [ ] Did you follow our [style guides](https://apluslms.github.io/contribute/styleguides/)?\r\n- [ ] Did you use Python type hinting in all functions that you added or edited? ([type hints](https://docs.python.org/3/library/typing.html) for function parameters and return values)\r\n\r\n# Have you updated the README or other relevant documentation?\r\n\r\n- [ ] documents inside the doc directory.\r\n- [ ] README.md.\r\n- [ ] Aplus Manual.\r\n- [ ] Other documentation (mention below which documentation).\r\n\r\n# Is it Done?\r\n\r\n- [ ] Reviewer has finished the code review\r\n- [ ] After the review, the developer has made changes accordingly\r\n- [ ] Customer/Teacher has accepted the implementation of the feature\r\n\r\n*Clean up your git commit history before submitting the pull request!*\r\n\n", "before_files": [{"content": "\"\"\"\nBase permission classes.\n\nThese classes use same interface than ones in django-rest-framework and\nare usable with APIViews too.\n\"\"\"\nimport string\n\nfrom django.utils.text import format_lazy\nfrom django.utils.translation import gettext_lazy as _\nfrom rest_framework.permissions import BasePermission as Permission\n\nfrom lib.helpers import Enum\n\n\nSAFE_METHODS = ('GET', 'HEAD', 'OPTIONS')\n\n\nclass FilterBackend:\n    \"\"\"\n    FilterBackend interface\n    \"\"\"\n    def filter_queryset(self, request, queryset, view):\n        \"\"\"\n        Return a filtered queryset.\n        \"\"\"\n        raise NotImplementedError\n\n    def get_fields(self, view): # pylint: disable=unused-argument\n        return []\n\n\nclass NoPermission(Permission):\n    \"\"\"\n    Base Permission class that gives no access permission to anyone.\n    \"\"\"\n    def has_permission(self, request, view):\n        return False\n\n    def has_object_permission(self, request, view, obj):\n        return False\n\n\nclass MessageMixin:\n    \"\"\"\n    Adds easy way to specify what exactly caused the PermissionDenied\n    \"\"\"\n    def error_msg(self, message: str, delim=None, format=None, replace=False): # pylint: disable=redefined-builtin\n        \"\"\"\n        Add extra text to self.message about the reason why permission\n        was denied. Uses lazy object so the message string is evaluated\n        only when rendered.\n\n        If optional argument `format` is given, then it's used with format_lazy\n        to format the message with the dictionary arguments from `format` arg.\n\n        Optional argument `delim` can be used to change the string used to join\n        self.message and `message`.\n\n        If optional argument `replace` is true, then self.message is replaced with\n        the `message`.\n        \"\"\"\n        if delim is None:\n            delim = ': '\n\n        if format:\n            message = format_lazy(message, **format)\n\n        if replace:\n            self.message = message\n        else:\n            assert 'message' not in self.__dict__, (\n                \"You are calling error_msg without replace=True \"\n                \"after calling it with it first. Fix your code by removing \"\n                \"the first method call and add replace=True to the second method call too.\"\n            )\n            msg_without_end_punctuation = (\n                self.message[0:-1] if self.message[-1] in string.punctuation\n                else self.message\n            )\n            self.message = format_lazy(\n                '{}{}{}',\n                msg_without_end_punctuation,\n                delim,\n                message,\n            )\n\n\n# Access mode\n# ===========\n\n# All access levels\nACCESS = Enum(\n    ('ANONYMOUS', 0, _('ACCESS_ANYONE')),\n    ('ENROLL', 1, None),\n    ('STUDENT', 3, _('ACCESS_ANY_STUDENT')),\n    ('ENROLLED', 4, _('ACCESS_ENROLLED_STUDENT')),\n    ('ASSISTANT', 5, _('ACCESS_COURSE_ASSISTANT')),\n    ('GRADING', 6, _('ACCESS_GRADING')),\n    ('TEACHER', 10, _('ACCESS_TEACHER')),\n    ('SUPERUSER', 100, _('ACCESS_SUPERUSER')),\n)\n\n\nclass AccessModePermission(MessageMixin, Permission):\n    \"\"\"\n    If view has access_mode that is not anonymous, then require authentication\n    \"\"\"\n    message = _('ACCESS_PERMISSION_DENIED_MSG')\n\n    def has_permission(self, request, view):\n        access_mode = view.get_access_mode()\n\n        if access_mode == ACCESS.ANONYMOUS:\n            return True\n        if not request.user.is_authenticated:\n            return False\n\n        if access_mode >= ACCESS.SUPERUSER:\n            return request.user.is_superuser\n\n        if access_mode >= ACCESS.TEACHER:\n            if not view.is_teacher:\n                self.error_msg(_('ACCESS_ERROR_ONLY_TEACHERS'))\n                return False\n\n        elif access_mode >= ACCESS.ASSISTANT:\n            if not view.is_course_staff:\n                self.error_msg(_('ACCESS_ERROR_ONLY_COURSE_STAFF'))\n                return False\n\n        elif access_mode == ACCESS.ENROLLED:\n            if not view.is_course_staff and not view.is_student:\n                self.error_msg(_('ACCESS_ERROR_ONLY_ENROLLED_STUDENTS'))\n                return False\n\n        return True\n\n\n# Object permissions\n# ==================\n\n\nclass ObjectVisibleBasePermission(MessageMixin, Permission):\n    model = None\n    obj_var = None\n\n    def has_permission(self, request, view):\n        obj = getattr(view, self.obj_var, None)\n        return (\n            obj is None or\n            self.has_object_permission(request, view, obj)\n        )\n\n    def has_object_permission(self, request, view, obj):\n        user = request.user\n        return (\n            # skip objects that are not the model in question\n            not isinstance(obj, self.model) or # pylint: disable=isinstance-second-argument-not-valid-type\n            user.is_staff or\n            user.is_superuser or\n            self.is_object_visible(request, view, obj)\n        )\n\n    def is_object_visible(self, request, view, obj):\n        raise NotImplementedError\n", "path": "authorization/permissions.py"}], "after_files": [{"content": "\"\"\"\nBase permission classes.\n\nThese classes use same interface than ones in django-rest-framework and\nare usable with APIViews too.\n\"\"\"\nimport string\n\nfrom django.utils.text import format_lazy\nfrom django.utils.translation import gettext_lazy as _\nfrom rest_framework.permissions import BasePermission as Permission\n\nfrom lib.helpers import Enum\n\n\nSAFE_METHODS = ('GET', 'HEAD', 'OPTIONS')\n\n\nclass FilterBackend:\n    \"\"\"\n    FilterBackend interface\n    \"\"\"\n    def filter_queryset(self, request, queryset, view):\n        \"\"\"\n        Return a filtered queryset.\n        \"\"\"\n        raise NotImplementedError\n\n    def get_fields(self, view): # pylint: disable=unused-argument\n        return []\n\n\nclass NoPermission(Permission):\n    \"\"\"\n    Base Permission class that gives no access permission to anyone.\n    \"\"\"\n    def has_permission(self, request, view):\n        return False\n\n    def has_object_permission(self, request, view, obj):\n        return False\n\n\nclass MessageMixin:\n    \"\"\"\n    Adds easy way to specify what exactly caused the PermissionDenied\n    \"\"\"\n    def error_msg(self, message: str, delim=None, format=None, replace=False): # pylint: disable=redefined-builtin\n        \"\"\"\n        Add extra text to self.message about the reason why permission\n        was denied. Uses lazy object so the message string is evaluated\n        only when rendered.\n\n        If optional argument `format` is given, then it's used with format_lazy\n        to format the message with the dictionary arguments from `format` arg.\n\n        Optional argument `delim` can be used to change the string used to join\n        self.message and `message`.\n\n        If optional argument `replace` is true, then self.message is replaced with\n        the `message`.\n        \"\"\"\n        if delim is None:\n            delim = ': '\n\n        if format:\n            message = format_lazy(message, **format)\n\n        if replace:\n            self.message = message\n        else:\n            assert 'message' not in self.__dict__, (\n                \"You are calling error_msg without replace=True \"\n                \"after calling it with it first. Fix your code by removing \"\n                \"the first method call and add replace=True to the second method call too.\"\n            )\n            msg_without_end_punctuation = (\n                self.message[0:-1] if self.message[-1] in string.punctuation\n                else self.message\n            )\n            self.message = format_lazy(\n                '{}{}{}',\n                msg_without_end_punctuation,\n                delim,\n                message,\n            )\n\n\n# Access mode\n# ===========\n\n# All access levels\nACCESS = Enum(\n    ('ANONYMOUS', 0, _('ACCESS_ANYONE')),\n    ('ENROLL', 1, None),\n    ('STUDENT', 3, _('ACCESS_ANY_STUDENT')),\n    ('ENROLLED', 4, _('ACCESS_ENROLLED_STUDENT')),\n    ('ASSISTANT', 5, _('ACCESS_COURSE_ASSISTANT')),\n    ('GRADING', 6, _('ACCESS_GRADING')),\n    ('TEACHER', 10, _('ACCESS_TEACHER')),\n    ('SUPERUSER', 100, _('ACCESS_SUPERUSER')),\n)\n\n\nclass AccessModePermission(MessageMixin, Permission):\n    \"\"\"\n    If view has access_mode that is not anonymous, then require authentication\n    \"\"\"\n    message = _('ACCESS_PERMISSION_DENIED_MSG')\n\n    def has_permission(self, request, view):\n        access_mode = view.get_access_mode()\n\n        if access_mode == ACCESS.ANONYMOUS:\n            return True\n        if not request.user.is_authenticated:\n            self.error_msg(_('ACCESS_ERROR_ONLY_AUTHENTICATED'))\n            return False\n\n        if access_mode >= ACCESS.SUPERUSER:\n            return request.user.is_superuser\n\n        if access_mode >= ACCESS.TEACHER:\n            if not view.is_teacher:\n                self.error_msg(_('ACCESS_ERROR_ONLY_TEACHERS'))\n                return False\n\n        elif access_mode >= ACCESS.ASSISTANT:\n            if not view.is_course_staff:\n                self.error_msg(_('ACCESS_ERROR_ONLY_COURSE_STAFF'))\n                return False\n\n        elif access_mode == ACCESS.ENROLLED:\n            if not view.is_course_staff and not view.is_student:\n                self.error_msg(_('ACCESS_ERROR_ONLY_ENROLLED_STUDENTS'))\n                return False\n\n        return True\n\n\n# Object permissions\n# ==================\n\n\nclass ObjectVisibleBasePermission(MessageMixin, Permission):\n    model = None\n    obj_var = None\n\n    def has_permission(self, request, view):\n        obj = getattr(view, self.obj_var, None)\n        return (\n            obj is None or\n            self.has_object_permission(request, view, obj)\n        )\n\n    def has_object_permission(self, request, view, obj):\n        user = request.user\n        return (\n            # skip objects that are not the model in question\n            not isinstance(obj, self.model) or # pylint: disable=isinstance-second-argument-not-valid-type\n            user.is_staff or\n            user.is_superuser or\n            self.is_object_visible(request, view, obj)\n        )\n\n    def is_object_visible(self, request, view, obj):\n        raise NotImplementedError\n", "path": "authorization/permissions.py"}]}