diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 403d2944ecfb87b125e0f1a33e3ce1af37fa6c5e..99ed48344cd7bdea19fb27982552b07431eb12cc 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -9,6 +9,14 @@ and this project adheres to `Semantic Versioning`_.
 Unreleased
 ----------
 
+Fixed
+~~~~~
+
+* Signup was forbidden even if it was enabled in settings
+* Phone numbers were not properly linked and suboptimally formatted on person page
+* Favicon upload failed with S3 storage.
+* Some preferences were required when they shouldn't, and vice versa.
+
 `2.6`_ - 2022-01-10
 -------------------
 
diff --git a/aleksis/core/apps.py b/aleksis/core/apps.py
index d37a1c616953013bec122aa8070ba0d6a389a921..ab264c53a91edac303e8cb574e7d7a56fdca00ac 100644
--- a/aleksis/core/apps.py
+++ b/aleksis/core/apps.py
@@ -17,7 +17,7 @@ from .registries import (
     site_preferences_registry,
 )
 from .util.apps import AppConfig
-from .util.core_helpers import get_or_create_favicon, has_person
+from .util.core_helpers import get_or_create_favicon, get_site_preferences, has_person
 from .util.sass_helpers import clean_scss
 
 
@@ -103,11 +103,15 @@ class CoreConfig(AppConfig):
                 is_favicon = name == "favicon"
 
                 if new_value:
-                    favicon_id = Favicon.on_site.update_or_create(
+                    # Get file object from preferences instead of using new_value
+                    # to prevent problems with special file storages
+                    file_obj = get_site_preferences()[f"{section}__{name}"]
+
+                    favicon = Favicon.on_site.update_or_create(
                         title=name,
-                        defaults={"isFavicon": is_favicon, "faviconImage": new_value},
+                        defaults={"isFavicon": is_favicon, "faviconImage": file_obj},
                     )[0]
-                    FaviconImg.objects.filter(faviconFK=favicon_id).delete()
+                    FaviconImg.objects.filter(faviconFK=favicon).delete()
                 else:
                     Favicon.on_site.filter(title=name, isFavicon=is_favicon).delete()
                     if name in settings.DEFAULT_FAVICON_PATHS:
diff --git a/aleksis/core/models.py b/aleksis/core/models.py
index 8bb6def6cc619e6729e73b4760ec329b77c46a07..04632a5615cd701cbae9e9d7f3a89b92ff82c74e 100644
--- a/aleksis/core/models.py
+++ b/aleksis/core/models.py
@@ -860,40 +860,7 @@ class AnnouncementRecipient(ExtensibleModel):
 
 
 class DashboardWidget(PolymorphicModel, PureDjangoModel):
-    """Base class for dashboard widgets on the index page.
-
-    To implement a widget, add a model that subclasses DashboardWidget, sets the template
-    and implements the get_context method to return a dictionary to be passed as context
-    to the template.
-
-    If your widget does not add any database fields, you should mark it as a proxy model.
-
-    You can provide a Media meta class with custom JS and CSS files which
-    will be added to html head.  For further information on media definition
-    see https://docs.djangoproject.com/en/3.0/topics/forms/media/
-
-    Example::
-
-      from django.forms.widgets import Media
-
-      from aleksis.core.models import DashboardWidget
-
-      class MyWidget(DashboardWidget):
-          template = "myapp/widget.html"
-
-          def get_context(self, request):
-              context = {"some_content": "foo"}
-              return context
-
-          class Meta:
-              proxy = True
-
-          media = Media(css={
-                  'all': ('pretty.css',)
-              },
-              js=('animations.js', 'actions.js')
-          )
-    """
+    """Base class for dashboard widgets on the index page."""
 
     objects = UninstallRenitentPolymorphicManager()
 
diff --git a/aleksis/core/preferences.py b/aleksis/core/preferences.py
index cb351100a5da31c4d5cffc2255542bc86f4a77c1..fe29f220f211616bf57d08c04e87a95b60c84365 100644
--- a/aleksis/core/preferences.py
+++ b/aleksis/core/preferences.py
@@ -40,8 +40,8 @@ class SiteTitle(StringPreference):
     section = general
     name = "title"
     default = "AlekSIS"
-    required = False
     verbose_name = _("Site title")
+    required = True
 
 
 @site_preferences_registry.register
@@ -62,9 +62,9 @@ class ColourPrimary(StringPreference):
     section = theme
     name = "primary"
     default = "#0d5eaf"
-    required = False
     verbose_name = _("Primary colour")
     widget = ColorWidget
+    required = True
 
 
 @site_preferences_registry.register
@@ -74,9 +74,9 @@ class ColourSecondary(StringPreference):
     section = theme
     name = "secondary"
     default = "#0d5eaf"
-    required = False
     verbose_name = _("Secondary colour")
     widget = ColorWidget
+    required = True
 
 
 @site_preferences_registry.register
@@ -87,6 +87,7 @@ class Logo(PublicFilePreferenceMixin, FilePreference):
     field_class = ImageField
     name = "logo"
     verbose_name = _("Logo")
+    required = False
 
 
 @site_preferences_registry.register
@@ -97,6 +98,7 @@ class Favicon(PublicFilePreferenceMixin, FilePreference):
     field_class = ImageField
     name = "favicon"
     verbose_name = _("Favicon")
+    required = False
 
 
 @site_preferences_registry.register
@@ -107,6 +109,7 @@ class PWAIcon(PublicFilePreferenceMixin, FilePreference):
     field_class = ImageField
     name = "pwa_icon"
     verbose_name = _("PWA-Icon")
+    required = False
 
 
 @site_preferences_registry.register
@@ -116,8 +119,8 @@ class MailOutName(StringPreference):
     section = mail
     name = "name"
     default = "AlekSIS"
-    required = False
     verbose_name = _("Mail out name")
+    required = True
 
 
 @site_preferences_registry.register
@@ -127,9 +130,9 @@ class MailOut(StringPreference):
     section = mail
     name = "address"
     default = settings.DEFAULT_FROM_EMAIL
-    required = False
     verbose_name = _("Mail out address")
     field_class = EmailField
+    required = True
 
 
 @site_preferences_registry.register
@@ -163,12 +166,12 @@ class AdressingNameFormat(ChoicePreference):
     section = notification
     name = "addressing_name_format"
     default = "first_last"
-    required = False
     verbose_name = _("Name format for addressing")
     choices = (
         ("first_last", "John Doe"),
         ("last_fist", "Doe, John"),
     )
+    required = True
 
 
 @person_preferences_registry.register
@@ -300,6 +303,7 @@ class OAuthAllowedGrants(MultipleChoicePreference):
     verbose_name = _("Allowed Grant Flows for OAuth applications")
     field_attribute = {"initial": []}
     choices = AbstractApplication.GRANT_TYPES
+    required = False
 
 
 @site_preferences_registry.register
@@ -313,6 +317,7 @@ class AvailableLanguages(MultipleChoicePreference):
     verbose_name = _("Available languages")
     field_attribute = {"initial": []}
     choices = settings.LANGUAGES
+    required = True
 
 
 @site_preferences_registry.register
@@ -376,6 +381,7 @@ class EditableFieldsPerson(MultipleChoicePreference):
     verbose_name = _("Fields on person model which are editable by themselves.")
     field_attribute = {"initial": []}
     choices = [(field.name, field.name) for field in Person.syncable_fields()]
+    required = False
 
 
 @site_preferences_registry.register
@@ -391,6 +397,7 @@ class SendNotificationOnPersonChange(MultipleChoicePreference):
     )
     field_attribute = {"initial": []}
     choices = [(field.name, field.name) for field in Person.syncable_fields()]
+    required = False
 
 
 @site_preferences_registry.register
@@ -401,6 +408,7 @@ class PersonChangeNotificationContact(StringPreference):
     name = "person_change_notification_contact"
     default = ""
     verbose_name = _("Contact for notification if a person changes their data")
+    required = False
 
 
 @site_preferences_registry.register
diff --git a/aleksis/core/templates/core/person/full.html b/aleksis/core/templates/core/person/full.html
index 749ba164229845057c965e4efd3e8d0fb9423f32..24e267db3caf7b6a9886f10269a4db654514f696 100644
--- a/aleksis/core/templates/core/person/full.html
+++ b/aleksis/core/templates/core/person/full.html
@@ -164,14 +164,22 @@
               <i class="material-icons small">phone</i>
             </td>
             <td>
-              <a href="tel:{{ person.phone_number }}">{{ person.phone_number }}</a>
-              <small>({% trans "home number" %})</small>
+              {% if person.phone_number %}
+                <a href="{{ person.phone_number.as_rfc3966 }}">{{ person.phone_number.as_international }}</a>
+              {% else %}
+                –
+              {% endif %}
+              <small>({% trans "Home phone" %})</small>
             </td>
           </tr>
           <tr>
             <td>
-              <a href="tel:{{ person.phone_number }}">{{ person.mobile_number }}</a>
-              <small>({% trans "mobile number" %})</small>
+              {% if person.mobile_number %}
+                <a href="{{ person.mobile_number.as_rfc3966 }}">{{ person.mobile_number.as_international }}</a>
+              {% else %}
+                –
+              {% endif %}
+              <small>({% trans "Mobile phone" %})</small>
             </td>
           </tr>
           <tr>
diff --git a/aleksis/core/views.py b/aleksis/core/views.py
index 45167450e83746e97bb65c4027238676c098b1c8..ec38875ad876f6cc7f6b8943a70254a28704195b 100644
--- a/aleksis/core/views.py
+++ b/aleksis/core/views.py
@@ -56,7 +56,6 @@ from oauth2_provider.models import get_application_model
 from oauth2_provider.views import AuthorizationView
 from reversion import set_user
 from reversion.views import RevisionMixin
-from rules import test_rule
 from rules.contrib.views import PermissionRequiredMixin, permission_required
 from two_factor.views.core import LoginView as AllAuthLoginView
 
@@ -1409,7 +1408,9 @@ class AccountRegisterView(SignupView):
     success_url = "index"
 
     def dispatch(self, request, *args, **kwargs):
-        if not test_rule("core.can_register") and not request.session.get("account_verified_email"):
+        if not request.user.has_perm("core.can_register") and not request.session.get(
+            "account_verified_email"
+        ):
             raise PermissionDenied()
         return super(AccountRegisterView, self).dispatch(request, *args, **kwargs)
 
diff --git a/docs/_static/create_dashboard_widget.png b/docs/_static/create_dashboard_widget.png
new file mode 100644
index 0000000000000000000000000000000000000000..31cfcb31474aaf5b97b88e1e818f0c8363dda1a7
Binary files /dev/null and b/docs/_static/create_dashboard_widget.png differ
diff --git a/docs/_static/dashboard.png b/docs/_static/dashboard.png
new file mode 100644
index 0000000000000000000000000000000000000000..a25e5ca64139e80a35e05a07bffe3b7112113139
Binary files /dev/null and b/docs/_static/dashboard.png differ
diff --git a/docs/_static/dashboard_widgets.png b/docs/_static/dashboard_widgets.png
new file mode 100644
index 0000000000000000000000000000000000000000..7e345543a30489b902a261296fe53de8d2c67e30
Binary files /dev/null and b/docs/_static/dashboard_widgets.png differ
diff --git a/docs/_static/edit_dashboard.png b/docs/_static/edit_dashboard.png
new file mode 100644
index 0000000000000000000000000000000000000000..92a277482c91ecb778fd3651d9351dc97221c3a1
Binary files /dev/null and b/docs/_static/edit_dashboard.png differ
diff --git a/docs/_static/edit_default_dashboard.png b/docs/_static/edit_default_dashboard.png
new file mode 100644
index 0000000000000000000000000000000000000000..c7547249c4152b433b984f5f40a258cf149306df
Binary files /dev/null and b/docs/_static/edit_default_dashboard.png differ
diff --git a/docs/admin/10_dashboard.rst b/docs/admin/10_dashboard.rst
new file mode 100644
index 0000000000000000000000000000000000000000..0c7f6a3b5a5ebda81f417b1a86a1135540a580ba
--- /dev/null
+++ b/docs/admin/10_dashboard.rst
@@ -0,0 +1,92 @@
+Providing important information to users using the dashboard
+============================================================
+
+The dashboard is a central place for providing important information to users.
+This is done by so-called dashboard widgets provided by the Core and apps.
+
+Built-in dashboard widgets
+--------------------------
+
+External link widget
+^^^^^^^^^^^^^^^^^^^^
+
+The external link widget will show a link to an external site on the dashboard,
+optionally with an icon or picture next to it. It therefore provides the following additional attributes:
+
+* **URL**: The URL of the external site.
+* **Icon URL**: The URL of the icon or picture shown next to the link.
+
+As link title, the widget title will be used.
+
+More dashboard widgets from apps
+--------------------------------
+
+In addition to the built-in widgets, apps can provide their own dashboard widgets.
+Best examples for such apps are currently *AlekSIS-App-DashboardFeeds* and *AlekSIS-App-Chronos*.
+
+.. Add References to the apps
+
+.. _core-configure-dashboard-widgets:
+
+Add and configure dashboard widgets
+-----------------------------------
+
+If you want to add a new dashboard widget, you can do so by adding the dashboard widget at *Admin → Dashboard widgets*.
+There you will see all currently configured dashboard widgets and
+can add new ones using the *Create dashboard widget* button which will ask your for the widget type.
+
+.. image:: ../_static/dashboard_widgets.png
+  :width: 100%
+  :alt: All configured dashboard widgets
+
+Each dashboard widget has at least the followong attributes
+
+* **Widget Title**: The title of the widget (will be shown in some widgets).
+* **Activate Widget**: If this isn't checked, the widget will not be shown.
+* **Widget is broken**: If this is checked, the widget will be shown
+  but the user will get a message that this widget is currently out of order because of an error.
+  This shouldn't be checked by yourself, but might be activated automatically by a widget if it encounters an error.
+  If this case enters, you should check for the cause of the error and fix it. After that, you can unmark the widget as broken.
+* **Size on different screens**: The size of the widget on different screens.
+  We work with a grid system containing a maximum of 12 columns. So, one column is 1/12 of the screen width.
+  The width in the following fields has to be entered as number of columns (1 to 12).
+
+  * **Size on mobile devices**: The size of the widget on mobile devices (600px and less).
+  * **Size on tablet devices**: The size of the widget on desktop devices (600px - 992px).
+  * **Size on desktop devices**: The size of the widget on desktop devices (992px - 1200px).
+  * **Size on large desktop devices**: The size of the widget on large desktop devices (1200px and above).
+
+All other attributes are specific to the widget type and are explained in the documentation of the widget.
+
+.. image:: ../_static/create_dashboard_widget.png
+  :width: 100%
+  :alt: Form to create an external link widget
+
+Setup a default dashboard
+-------------------------
+
+To make the configured dashboard widgets accessible to all users, we recommend to configure the default dashboard.
+If you don't do so, the dashboard widgets will only be available to users if they customise their dashboard.
+
+The default dashboard can be configured via *Admin → Dashboard widgets → Edit default dashboard*.
+The edit page works exactly as the page described in :ref:`core-user-customising-dashboard`.
+
+.. image:: ../_static/edit_default_dashboard.png
+  :width: 100%
+  :alt: Edit the default dashboard
+
+Preferences
+-----------
+
+The behavior of the dashboard can be configured via *Admin → Configuration → General*. The following settings are available:
+
+* **Show dashboard to users without login**: If this is checked, the dashboard will be also shown to users who are not logged in.
+
+.. warning::
+
+    That won't work with all dashboard widgets. Some widgets, like the timetable widgets, require a logged in user.
+
+* **Allow users to edit their dashboard**: With this preference, system administrators can decide whether users
+  can edit their own dashboard as described in :ref:`core-user-customising-dashboard`.
+* **Automatically update the dashboard and its widgets sitewide**: If enabled,
+  the dashboard will be updated automatically every 15 seconds.
diff --git a/docs/dev/10_dashboard_widgets.rst b/docs/dev/10_dashboard_widgets.rst
new file mode 100644
index 0000000000000000000000000000000000000000..434c8c9a78cd2f8664f2a783ea997f0d4e1b6df1
--- /dev/null
+++ b/docs/dev/10_dashboard_widgets.rst
@@ -0,0 +1,40 @@
+Registering dashboard widgets
+=============================
+
+Apps can register their own dashboard widgets which are automatically registered in the corresponding frontend for
+configuring them.
+
+To implement a widget, add a model that subclasses ``DashboardWidget``, set the template
+and implement the ``get_context`` method to return a dictionary to be passed as context
+to the template. The template system works as in every Django view and allows you to use the normal Django
+template language.
+
+If your widget does not add any custom database fields, you should mark it as a proxy model.
+
+You can provide a ``Media`` meta class with custom JS and CSS files which
+will be added to the HTML head on the dashboard if the dashboard widget is shown.
+For further information on media definition, see `Django Media`_.
+
+Example::
+
+  from django.forms.widgets import Media
+
+  from aleksis.core.models import DashboardWidget
+
+  class MyWidget(DashboardWidget):
+      template = "myapp/widget.html"
+
+      def get_context(self, request):
+          context = {"some_content": "foo"}
+          return context
+
+      class Meta:
+          proxy = True
+
+      media = Media(css={
+              'all': ('pretty.css',)
+          },
+          js=('animations.js', 'actions.js')
+      )
+
+.. _Django Media: https://docs.djangoproject.com/en/3.0/topics/forms/media/
diff --git a/docs/user/02_dashboard.rst b/docs/user/02_dashboard.rst
new file mode 100644
index 0000000000000000000000000000000000000000..20eae0260650350a23c2e64ce39ffbe36c33142d
--- /dev/null
+++ b/docs/user/02_dashboard.rst
@@ -0,0 +1,43 @@
+Dashboard
+=========
+
+The first thing you will see after the login is the dashboard.
+Depending on what your system administrator configured,
+you will be able to see information from different apps at one glance.
+
+.. image:: ../_static/dashboard.png
+  :width: 100%
+  :alt: The dashboard
+
+Dashboard widgets
+-----------------
+
+The dashboard consists of different parts, the so-called *dashboard widgets*.
+They are configured by the system administrator and can be freely
+arranged on the dashboard (cf. :ref:`core-user-customising-dashboard`).
+
+.. _core-user-customising-dashboard:
+
+Customising the dashboard
+-------------------------
+
+There are several options for customising your personal dashboard. By default,
+you will see a layout provided by your system administrator. Using the button
+*Edit dashboard* on the top right corner of the dashboard,
+you can change the selection and position of the widgets.
+
+.. image:: ../_static/edit_dashboard.png
+  :width: 100%
+  :alt: Edit the dashboard
+
+On the edit page, you will see a list of all available widgets and your current dashboard.
+If the section *Your dashboard* is empty, the default dashboard will be shown.
+To make an own layout, you can drag widgets from the *Available widgets* to *Your dashboard*.
+Within *Your dashboard* you also can arrange the widgets by dragging them.
+To remove widgets from the dashboard, you just have to drag them back to *Available widgets*.
+
+In addition to editing the dashboard, you can also change same preferences referring to the dashboard.
+This is done under the menu item *Account → Preferences → General*:
+
+* **Automatically update the dashboard and its widgets:** If enabled by you and the system administrator,
+  the dashboard will be updated automatically every 15 seconds.
diff --git a/pyproject.toml b/pyproject.toml
index 4bd523cd499c3c944cceeb8ded9b91a06a69be9a..8254a6a2a05a9790ea3babfad998f58636cce6f1 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -112,7 +112,7 @@ django-cleavejs = "^0.1.0"
 django-allauth = "^0.47.0"
 django-uwsgi-ng = "^1.1.0"
 django-extensions = "^3.1.1"
-ipython = "^7.20.0"
+ipython = "^8.0.0"
 django-oauth-toolkit = "^1.6.2"
 django-redis = "^5.0.0"
 django-storages = {version = "^1.11.1", optional = true}