Repository: digitalfabrik/integreat-cms

Issue:
Password reset not working
### Describe the Bug
<!-- A clear and concise description of what the bug is. -->
The password reset does not work at the moment

### Steps to Reproduce

1. Go to login form
2. Try to reset password
3. See error

### Expected Behavior
<!-- A clear and concise description of what you expected to happen. -->
The user should get an email with the password reset link

### Actual Behavior
<!-- A clear and concise description of what actually happened. -->
A `NoReverseMatch` at `/reset-password/` appears.

### Additional Information
<!-- Add any other context (e.g. logs, screenshots, etc.) about the problem here. -->
<details>
  <summary>Traceback</summary>
    
  ```javascript
Feb 16 09:19:34 ERROR   django.request - Internal Server Error: /reset-password/
Traceback (most recent call last):
  File "/opt/integreat-cms/.venv/lib/python3.9/site-packages/django/core/handlers/exception.py", line 47, in inner
    response = get_response(request)
  File "/opt/integreat-cms/.venv/lib/python3.9/site-packages/django/core/handlers/base.py", line 181, in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
  File "/opt/integreat-cms/.venv/lib/python3.9/site-packages/django/views/generic/base.py", line 70, in view
    return self.dispatch(request, *args, **kwargs)
  File "/opt/integreat-cms/.venv/lib/python3.9/site-packages/integreat_cms/cms/views/authentication/password_reset_view.py", line 46, in dispatch
    return super().dispatch(*args, **kwargs)
  File "/opt/integreat-cms/.venv/lib/python3.9/site-packages/django/utils/decorators.py", line 43, in _wrapper
    return bound_method(*args, **kwargs)
  File "/opt/integreat-cms/.venv/lib/python3.9/site-packages/django/utils/decorators.py", line 130, in _wrapped_view
    response = view_func(request, *args, **kwargs)
  File "/opt/integreat-cms/.venv/lib/python3.9/site-packages/django/contrib/auth/views.py", line 222, in dispatch
    return super().dispatch(*args, **kwargs)
  File "/opt/integreat-cms/.venv/lib/python3.9/site-packages/django/views/generic/base.py", line 98, in dispatch
    return handler(request, *args, **kwargs)
  File "/opt/integreat-cms/.venv/lib/python3.9/site-packages/django/views/generic/edit.py", line 142, in post
    return self.form_valid(form)
  File "/opt/integreat-cms/.venv/lib/python3.9/site-packages/integreat_cms/cms/views/authentication/password_reset_view.py", line 78, in form_valid
    return super().form_valid(form)
  File "/opt/integreat-cms/.venv/lib/python3.9/site-packages/django/contrib/auth/views.py", line 235, in form_valid
    form.save(**opts)
  File "/opt/integreat-cms/.venv/lib/python3.9/site-packages/django/contrib/auth/forms.py", line 310, in save
    self.send_mail(
  File "/opt/integreat-cms/.venv/lib/python3.9/site-packages/django/contrib/auth/forms.py", line 253, in send_mail
    body = loader.render_to_string(email_template_name, context)
  File "/opt/integreat-cms/.venv/lib/python3.9/site-packages/django/template/loader.py", line 62, in render_to_string
    return template.render(context, request)
  File "/opt/integreat-cms/.venv/lib/python3.9/site-packages/django/template/backends/django.py", line 61, in render
    return self.template.render(context)
  File "/opt/integreat-cms/.venv/lib/python3.9/site-packages/django/template/base.py", line 170, in render
    return self._render(context)
  File "/opt/integreat-cms/.venv/lib/python3.9/site-packages/django/template/base.py", line 162, in _render
    return self.nodelist.render(context)
  File "/opt/integreat-cms/.venv/lib/python3.9/site-packages/django/template/base.py", line 938, in render
    bit = node.render_annotated(context)
  File "/opt/integreat-cms/.venv/lib/python3.9/site-packages/django/template/base.py", line 905, in render_annotated
    return self.render(context)
  File "/opt/integreat-cms/.venv/lib/python3.9/site-packages/django/template/defaulttags.py", line 39, in render
    output = self.nodelist.render(context)
  File "/opt/integreat-cms/.venv/lib/python3.9/site-packages/django/template/base.py", line 938, in render
    bit = node.render_annotated(context)
  File "/opt/integreat-cms/.venv/lib/python3.9/site-packages/django/template/base.py", line 905, in render_annotated
    return self.render(context)
  File "/opt/integreat-cms/.venv/lib/python3.9/site-packages/django/template/defaulttags.py", line 449, in render
    url = reverse(view_name, args=args, kwargs=kwargs, current_app=current_app)
  File "/opt/integreat-cms/.venv/lib/python3.9/site-packages/django/urls/base.py", line 86, in reverse
    return resolver._reverse_with_prefix(view, prefix, *args, **kwargs)
  File "/opt/integreat-cms/.venv/lib/python3.9/site-packages/django/urls/resolvers.py", line 698, in _reverse_with_prefix
    raise NoReverseMatch(msg)
django.urls.exceptions.NoReverseMatch: Reverse for 'password_reset_confirm' with keyword arguments '{'uidb64': 'XXXX', 'token': 'XXXX'}' not found. 1 pattern(s) tried: ['reset\\-password/(?P<uidb64>[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\\-(?P<token>[^/]+)/\\Z']
  ```
  
</details>


Discussion:

Resolving patch:
diff --git a/integreat_cms/cms/urls/public.py b/integreat_cms/cms/urls/public.py
--- a/integreat_cms/cms/urls/public.py
+++ b/integreat_cms/cms/urls/public.py
@@ -86,7 +86,7 @@
                     name="password_reset",
                 ),
                 path(
-                    "<uuid:uidb64>-<token>/",
+                    "<uidb64>/<token>/",
                     authentication.PasswordResetConfirmView.as_view(),
                     name="password_reset_confirm",
                 ),
@@ -94,7 +94,7 @@
         ),
     ),
     path(
-        "activate-account/<uuid:uidb64>-<token>/",
+        "activate-account/<uidb64>/<token>/",
         authentication.AccountActivationView.as_view(),
         name="activate_account",
     ),
diff --git a/sphinx/conf.py b/sphinx/conf.py
--- a/sphinx/conf.py
+++ b/sphinx/conf.py
@@ -126,6 +126,7 @@
     ("py:class", "builtins.int"),
     ("py:class", "builtins.AssertionError"),
     ("py:class", "builtins.int"),
+    ("py:class", "NoneType"),
     ("py:class", "django.contrib.admin.helpers.ActionForm"),
     ("py:class", "django.contrib.admin.checks.ModelAdminChecks"),
     ("py:attr", "django.contrib.auth.models.Permission.user_set"),


Test patch:
diff --git a/tests/cms/views/view_config.py b/tests/cms/views/view_config.py
--- a/tests/cms/views/view_config.py
+++ b/tests/cms/views/view_config.py
@@ -28,6 +28,7 @@
             ("api:regions_live", ALL_ROLES),
             ("api:regions_hidden", ALL_ROLES),
             ("public:login_mfa", ALL_ROLES),
+            ("sitemap:index", ALL_ROLES),
             ("admin_dashboard", STAFF_ROLES),
             ("admin_feedback", STAFF_ROLES),
             ("languages", STAFF_ROLES),
@@ -108,6 +109,7 @@
             ("api:pages", ALL_ROLES),
             ("api:pdf_export", ALL_ROLES),
             ("api:sent_push_notifications", ALL_ROLES),
+            ("sitemap:region_language", ALL_ROLES),
             ("archived_pages", STAFF_ROLES + [MANAGEMENT, EDITOR]),
             ("archived_pois", ROLES),
             ("edit_imprint", ROLES),
@@ -599,3 +601,11 @@
     for view_conf, kwargs in REDIRECT_VIEWS
     for view_name, roles, target in view_conf
 ]
+
+#: Public views that only work for anonymous users
+PARAMETRIZED_PUBLIC_VIEWS = [
+    ("public:login", {}),
+    ("public:login_mfa", {}),
+    ("public:password_reset", {}),
+    ("public:password_reset", {"email": "root@root.root"}),
+]
diff --git a/tests/cms/views/view_test.py b/tests/cms/views/view_test.py
--- a/tests/cms/views/view_test.py
+++ b/tests/cms/views/view_test.py
@@ -1,10 +1,15 @@
 import pytest
 
 from django.conf import settings
+from django.test.client import Client
 from django.urls import reverse
 
 from ...conftest import ANONYMOUS
-from .view_config import PARAMETRIZED_VIEWS, PARAMETRIZED_REDIRECT_VIEWS
+from .view_config import (
+    PARAMETRIZED_VIEWS,
+    PARAMETRIZED_REDIRECT_VIEWS,
+    PARAMETRIZED_PUBLIC_VIEWS,
+)
 
 
 @pytest.mark.django_db
@@ -29,7 +34,7 @@ def test_view_status_code(login_role_user, view_name, kwargs, post_data, roles):
     :type roles: list
     """
     client, role = login_role_user
-    url = reverse(view_name, args=(), kwargs=kwargs)
+    url = reverse(view_name, kwargs=kwargs)
     if post_data:
         kwargs = {"data": post_data}
         # If the post data is a string, assume json as content type
@@ -84,8 +89,39 @@ def test_view_redirect(login_role_user, view_name, kwargs, roles, target):
     client, role = login_role_user
     # For redirection checks, we only test the given roles
     if role in roles:
-        url = reverse(view_name, args=(), kwargs=kwargs)
+        url = reverse(view_name, kwargs=kwargs)
         response = client.get(url)
         print(response.headers)
         assert response.status_code == 302
         assert response.headers.get("location") == target
+
+
+# pylint: disable=unused-argument
+@pytest.mark.django_db
+@pytest.mark.parametrize("view_name,post_data", PARAMETRIZED_PUBLIC_VIEWS)
+def test_public_view_status_code(load_test_data, view_name, post_data):
+    """
+    This test checks whether the given view return the correct status code for anonymous users
+
+    :param load_test_data: The fixture providing the test data (see :meth:`~tests.conftest.load_test_data`)
+    :type load_test_data: NoneType
+
+    :param view_name: The identifier of the view
+    :type view_name: str
+
+    :param post_data: The post data for this view
+    :type post_data: dict
+    """
+    client = Client()
+    url = reverse(view_name)
+    if post_data:
+        response = client.post(url, data=post_data)
+    else:
+        response = client.get(url)
+    print(response.headers)
+    if post_data:
+        # Post-views should redirect after a successful operation
+        assert response.status_code == 302
+    else:
+        # Get-views should return 200
+        assert response.status_code == 200
diff --git a/tests/conftest.py b/tests/conftest.py
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -61,7 +61,7 @@ def login_role_user(request, load_test_data, django_db_blocker):
     :type request: pytest.FixtureRequest
 
     :param load_test_data: The fixture providing the test data (see :meth:`~tests.conftest.load_test_data`)
-    :type load_test_data: tuple
+    :type load_test_data: NoneType
 
     :param django_db_blocker: The fixture providing the database blocker
     :type django_db_blocker: :fixture:`django_db_blocker`