diff --git a/{{cookiecutter.project_slug}}/.idea/runConfigurations/tests___all.xml b/{{cookiecutter.project_slug}}/.idea/runConfigurations/pytest___.xml similarity index 52% rename from {{cookiecutter.project_slug}}/.idea/runConfigurations/tests___all.xml rename to {{cookiecutter.project_slug}}/.idea/runConfigurations/pytest___.xml index be70ffcd6..08f76c454 100644 --- a/{{cookiecutter.project_slug}}/.idea/runConfigurations/tests___all.xml +++ b/{{cookiecutter.project_slug}}/.idea/runConfigurations/pytest___.xml @@ -1,18 +1,14 @@ - + - + \ No newline at end of file diff --git a/{{cookiecutter.project_slug}}/.idea/runConfigurations/tests___module__users.xml b/{{cookiecutter.project_slug}}/.idea/runConfigurations/pytest__users.xml similarity index 50% rename from {{cookiecutter.project_slug}}/.idea/runConfigurations/tests___module__users.xml rename to {{cookiecutter.project_slug}}/.idea/runConfigurations/pytest__users.xml index d838b5da6..574361fea 100644 --- a/{{cookiecutter.project_slug}}/.idea/runConfigurations/tests___module__users.xml +++ b/{{cookiecutter.project_slug}}/.idea/runConfigurations/pytest__users.xml @@ -1,18 +1,14 @@ - + - + \ No newline at end of file diff --git a/{{cookiecutter.project_slug}}/.idea/runConfigurations/tests___class__TestUser.xml b/{{cookiecutter.project_slug}}/.idea/runConfigurations/tests___class__TestUser.xml deleted file mode 100644 index 204de9dd1..000000000 --- a/{{cookiecutter.project_slug}}/.idea/runConfigurations/tests___class__TestUser.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - diff --git a/{{cookiecutter.project_slug}}/.idea/runConfigurations/tests___file__test_models.xml b/{{cookiecutter.project_slug}}/.idea/runConfigurations/tests___file__test_models.xml deleted file mode 100644 index ddb6d3d6d..000000000 --- a/{{cookiecutter.project_slug}}/.idea/runConfigurations/tests___file__test_models.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - diff --git a/{{cookiecutter.project_slug}}/.idea/runConfigurations/tests___specific__test_get_absolute_url.xml b/{{cookiecutter.project_slug}}/.idea/runConfigurations/tests___specific__test_get_absolute_url.xml deleted file mode 100644 index c391058f2..000000000 --- a/{{cookiecutter.project_slug}}/.idea/runConfigurations/tests___specific__test_get_absolute_url.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - diff --git a/{{cookiecutter.project_slug}}/requirements/local.txt b/{{cookiecutter.project_slug}}/requirements/local.txt index c67276107..83e8846c8 100644 --- a/{{cookiecutter.project_slug}}/requirements/local.txt +++ b/{{cookiecutter.project_slug}}/requirements/local.txt @@ -22,7 +22,6 @@ coverage==4.5.1 # https://github.com/nedbat/coveragepy # Django # ------------------------------------------------------------------------------ factory-boy==2.11.1 # https://github.com/FactoryBoy/factory_boy -django-test-plus==1.0.22 # https://github.com/revsys/django-test-plus django-debug-toolbar==1.9.1 # https://github.com/jazzband/django-debug-toolbar django-extensions==2.0.7 # https://github.com/django-extensions/django-extensions diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/conftest.py b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/conftest.py new file mode 100644 index 000000000..aae11d269 --- /dev/null +++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/conftest.py @@ -0,0 +1,20 @@ +import pytest +from django.conf import settings +from django.test import RequestFactory + +from {{ cookiecutter.project_slug }}.users.tests.factories import UserFactory + + +@pytest.fixture(autouse=True) +def media_storage(settings, tmpdir): + settings.MEDIA_ROOT = tmpdir.strpath + + +@pytest.fixture +def user() -> settings.AUTH_USER_MODEL: + return UserFactory() + + +@pytest.fixture +def request_factory() -> RequestFactory: + return RequestFactory() diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/adapters.py b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/adapters.py index 5b63593b2..9361d6eca 100644 --- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/adapters.py +++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/adapters.py @@ -1,15 +1,18 @@ -from django.conf import settings +from typing import Any + from allauth.account.adapter import DefaultAccountAdapter from allauth.socialaccount.adapter import DefaultSocialAccountAdapter +from django.conf import settings +from django.http import HttpRequest class AccountAdapter(DefaultAccountAdapter): - def is_open_for_signup(self, request): + def is_open_for_signup(self, request: HttpRequest): return getattr(settings, "ACCOUNT_ALLOW_REGISTRATION", True) class SocialAccountAdapter(DefaultSocialAccountAdapter): - def is_open_for_signup(self, request, sociallogin): + def is_open_for_signup(self, request: HttpRequest, sociallogin: Any): return getattr(settings, "ACCOUNT_ALLOW_REGISTRATION", True) diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/admin.py b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/admin.py index 8da8f86a5..cc6efed5f 100644 --- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/admin.py +++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/admin.py @@ -1,39 +1,17 @@ -from django import forms from django.contrib import admin -from django.contrib.auth.admin import UserAdmin as AuthUserAdmin -from django.contrib.auth.forms import UserChangeForm, UserCreationForm -from .models import User +from django.contrib.auth import admin as auth_admin +from django.contrib.auth import get_user_model +from {{ cookiecutter.project_slug }}.users.forms import UserChangeForm, UserCreationForm -class MyUserChangeForm(UserChangeForm): - - class Meta(UserChangeForm.Meta): - model = User - - -class MyUserCreationForm(UserCreationForm): - - error_message = UserCreationForm.error_messages.update( - {"duplicate_username": "This username has already been taken."} - ) - - class Meta(UserCreationForm.Meta): - model = User - - def clean_username(self): - username = self.cleaned_data["username"] - try: - User.objects.get(username=username) - except User.DoesNotExist: - return username - - raise forms.ValidationError(self.error_messages["duplicate_username"]) +User = get_user_model() @admin.register(User) -class MyUserAdmin(AuthUserAdmin): - form = MyUserChangeForm - add_form = MyUserCreationForm - fieldsets = (("User Profile", {"fields": ("name",)}),) + AuthUserAdmin.fieldsets - list_display = ("username", "name", "is_superuser") +class UserAdmin(auth_admin.UserAdmin): + + form = UserChangeForm + add_form = UserCreationForm + fieldsets = (("User", {"fields": ("name",)}),) + auth_admin.UserAdmin.fieldsets + list_display = ["username", "name", "is_superuser"] search_fields = ["name"] diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/apps.py b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/apps.py index 32fab76d7..854665fd3 100644 --- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/apps.py +++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/apps.py @@ -2,14 +2,11 @@ from django.apps import AppConfig class UsersAppConfig(AppConfig): - name = "{{cookiecutter.project_slug}}.users" + + name = "{{ cookiecutter.project_slug }}.users" verbose_name = "Users" def ready(self): - """Override this to put in: - Users system checks - Users signal registration - """ try: import users.signals # noqa F401 except ImportError: diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/forms.py b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/forms.py new file mode 100644 index 000000000..542e23efc --- /dev/null +++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/forms.py @@ -0,0 +1,30 @@ +from django.contrib.auth import get_user_model, forms +from django.core.exceptions import ValidationError + +User = get_user_model() + + +class UserChangeForm(forms.UserChangeForm): + + class Meta(forms.UserChangeForm.Meta): + model = User + + +class UserCreationForm(forms.UserCreationForm): + + error_message = forms.UserCreationForm.error_messages.update( + {"duplicate_username": "This username has already been taken."} + ) + + class Meta(forms.UserCreationForm.Meta): + model = User + + def clean_username(self): + username = self.cleaned_data["username"] + + try: + User.objects.get(username=username) + except User.DoesNotExist: + return username + + raise ValidationError(self.error_messages["duplicate_username"]) diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/models.py b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/models.py index 304758713..8f07b15a1 100644 --- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/models.py +++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/models.py @@ -1,5 +1,5 @@ from django.contrib.auth.models import AbstractUser -from django.db import models +from django.db.models import CharField from django.urls import reverse from django.utils.translation import ugettext_lazy as _ @@ -8,10 +8,7 @@ class User(AbstractUser): # First Name and Last Name do not cover name patterns # around the globe. - name = models.CharField(_("Name of User"), blank=True, max_length=255) - - def __str__(self): - return self.username + name = CharField(_("Name of User"), blank=True, max_length=255) def get_absolute_url(self): return reverse("users:detail", kwargs={"username": self.username}) diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/tests/factories.py b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/tests/factories.py index 8a871b64d..009905768 100644 --- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/tests/factories.py +++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/tests/factories.py @@ -1,11 +1,29 @@ -import factory +from typing import Any, Sequence + +from django.contrib.auth import get_user_model +from factory import DjangoModelFactory, Faker, post_generation -class UserFactory(factory.django.DjangoModelFactory): - username = factory.Sequence(lambda n: f"user-{n}") - email = factory.Sequence(lambda n: f"user-{n}@example.com") - password = factory.PostGenerationMethodCall("set_password", "password") +class UserFactory(DjangoModelFactory): + + username = Faker("user_name") + email = Faker("email") + name = Faker("name") + + @post_generation + def password(self, create: bool, extracted: Sequence[Any], **kwargs): + password = Faker( + "password", + length=42, + special_chars=True, + digits=True, + upper_case=True, + lower_case=True, + ).generate( + extra_kwargs={} + ) + self.set_password(password) class Meta: - model = "users.User" - django_get_or_create = ("username",) + model = get_user_model() + django_get_or_create = ["username"] diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/tests/test_admin.py b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/tests/test_admin.py deleted file mode 100644 index a3307103f..000000000 --- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/tests/test_admin.py +++ /dev/null @@ -1,44 +0,0 @@ -from test_plus.test import TestCase - -from ..admin import MyUserCreationForm - - -class TestMyUserCreationForm(TestCase): - - def setUp(self): - self.user = self.make_user("notalamode", "notalamodespassword") - - def test_clean_username_success(self): - # Instantiate the form with a new username - form = MyUserCreationForm( - { - "username": "alamode", - "password1": "7jefB#f@Cc7YJB]2v", - "password2": "7jefB#f@Cc7YJB]2v", - } - ) - # Run is_valid() to trigger the validation - valid = form.is_valid() - self.assertTrue(valid) - - # Run the actual clean_username method - username = form.clean_username() - self.assertEqual("alamode", username) - - def test_clean_username_false(self): - # Instantiate the form with the same username as self.user - form = MyUserCreationForm( - { - "username": self.user.username, - "password1": "notalamodespassword", - "password2": "notalamodespassword", - } - ) - # Run is_valid() to trigger the validation, which is going to fail - # because the username is already taken - valid = form.is_valid() - self.assertFalse(valid) - - # The form.errors dict should contain a single error called 'username' - self.assertTrue(len(form.errors) == 1) - self.assertTrue("username" in form.errors) diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/tests/test_forms.py b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/tests/test_forms.py new file mode 100644 index 000000000..e80661648 --- /dev/null +++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/tests/test_forms.py @@ -0,0 +1,41 @@ +import pytest + +from {{ cookiecutter.project_slug }}.users.forms import UserCreationForm +from {{ cookiecutter.project_slug }}.users.tests.factories import UserFactory + +pytestmark = pytest.mark.django_db + + +class TestUserCreationForm: + + def test_clean_username(self): + # A user with proto_user params does not exist yet. + proto_user = UserFactory.build() + + form = UserCreationForm( + { + "username": proto_user.username, + "password1": proto_user._password, + "password2": proto_user._password, + } + ) + + assert form.is_valid() + assert form.clean_username() == proto_user.username + + # Creating a user. + form.save() + + # The user with proto_user params already exists, + # hence cannot be created. + form = UserCreationForm( + { + "username": proto_user.username, + "password1": proto_user._password, + "password2": proto_user._password, + } + ) + + assert not form.is_valid() + assert len(form.errors) == 1 + assert "username" in form.errors diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/tests/test_models.py b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/tests/test_models.py index 13121a011..548636323 100644 --- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/tests/test_models.py +++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/tests/test_models.py @@ -1,16 +1,8 @@ -from test_plus.test import TestCase +import pytest +from django.conf import settings + +pytestmark = pytest.mark.django_db -class TestUser(TestCase): - - def setUp(self): - self.user = self.make_user() - - def test__str__(self): - self.assertEqual( - self.user.__str__(), - "testuser", # This is the default username for self.make_user() - ) - - def test_get_absolute_url(self): - self.assertEqual(self.user.get_absolute_url(), "/users/testuser/") +def test_user_get_absolute_url(user: settings.AUTH_USER_MODEL): + assert user.get_absolute_url() == f"/users/{user.username}/" diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/tests/test_urls.py b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/tests/test_urls.py index 6b072436d..20bd3dbad 100644 --- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/tests/test_urls.py +++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/tests/test_urls.py @@ -1,44 +1,28 @@ +import pytest +from django.conf import settings from django.urls import reverse, resolve -from test_plus.test import TestCase +pytestmark = pytest.mark.django_db -class TestUserURLs(TestCase): - """Test URL patterns for users app.""" +def test_detail(user: settings.AUTH_USER_MODEL): + assert ( + reverse("users:detail", kwargs={"username": user.username}) + == f"/users/{user.username}/" + ) + assert resolve(f"/users/{user.username}/").view_name == "users:detail" - def setUp(self): - self.user = self.make_user() - def test_list_reverse(self): - """users:list should reverse to /users/.""" - self.assertEqual(reverse("users:list"), "/users/") +def test_list(): + assert reverse("users:list") == "/users/" + assert resolve("/users/").view_name == "users:list" - def test_list_resolve(self): - """/users/ should resolve to users:list.""" - self.assertEqual(resolve("/users/").view_name, "users:list") - def test_redirect_reverse(self): - """users:redirect should reverse to /users/~redirect/.""" - self.assertEqual(reverse("users:redirect"), "/users/~redirect/") +def test_update(): + assert reverse("users:update") == "/users/~update/" + assert resolve("/users/~update/").view_name == "users:update" - def test_redirect_resolve(self): - """/users/~redirect/ should resolve to users:redirect.""" - self.assertEqual(resolve("/users/~redirect/").view_name, "users:redirect") - def test_detail_reverse(self): - """users:detail should reverse to /users/testuser/.""" - self.assertEqual( - reverse("users:detail", kwargs={"username": "testuser"}), "/users/testuser/" - ) - - def test_detail_resolve(self): - """/users/testuser/ should resolve to users:detail.""" - self.assertEqual(resolve("/users/testuser/").view_name, "users:detail") - - def test_update_reverse(self): - """users:update should reverse to /users/~update/.""" - self.assertEqual(reverse("users:update"), "/users/~update/") - - def test_update_resolve(self): - """/users/~update/ should resolve to users:update.""" - self.assertEqual(resolve("/users/~update/").view_name, "users:update") +def test_redirect(): + assert reverse("users:redirect") == "/users/~redirect/" + assert resolve("/users/~redirect/").view_name == "users:redirect" diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/tests/test_views.py b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/tests/test_views.py index 07cbda669..0992e4626 100644 --- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/tests/test_views.py +++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/tests/test_views.py @@ -1,52 +1,53 @@ +import pytest +from django.conf import settings from django.test import RequestFactory -from test_plus.test import TestCase +from {{ cookiecutter.project_slug }}.users.views import UserRedirectView, UserUpdateView -from ..views import UserRedirectView, UserUpdateView +pytestmark = pytest.mark.django_db -class BaseUserTestCase(TestCase): +class TestUserUpdateView: + """ + TODO: + extracting view initialization code as class-scoped fixture + would be great if only pytest-django supported non-function-scoped + fixture db access -- this is a work-in-progress for now: + https://github.com/pytest-dev/pytest-django/pull/258 + """ - def setUp(self): - self.user = self.make_user() - self.factory = RequestFactory() + def test_get_success_url( + self, user: settings.AUTH_USER_MODEL, request_factory: RequestFactory + ): + view = UserUpdateView() + request = request_factory.get("/fake-url/") + request.user = user - -class TestUserRedirectView(BaseUserTestCase): - - def test_get_redirect_url(self): - # Instantiate the view directly. Never do this outside a test! - view = UserRedirectView() - # Generate a fake request - request = self.factory.get("/fake-url") - # Attach the user to the request - request.user = self.user - # Attach the request to the view view.request = request - # Expect: '/users/testuser/', as that is the default username for - # self.make_user() - self.assertEqual(view.get_redirect_url(), "/users/testuser/") + + assert view.get_success_url() == f"/users/{user.username}/" + + def test_get_object( + self, user: settings.AUTH_USER_MODEL, request_factory: RequestFactory + ): + view = UserUpdateView() + request = request_factory.get("/fake-url/") + request.user = user + + view.request = request + + assert view.get_object() == user -class TestUserUpdateView(BaseUserTestCase): +class TestUserRedirectView: - def setUp(self): - # call BaseUserTestCase.setUp() - super(TestUserUpdateView, self).setUp() - # Instantiate the view directly. Never do this outside a test! - self.view = UserUpdateView() - # Generate a fake request - request = self.factory.get("/fake-url") - # Attach the user to the request - request.user = self.user - # Attach the request to the view - self.view.request = request + def test_get_redirect_url( + self, user: settings.AUTH_USER_MODEL, request_factory: RequestFactory + ): + view = UserRedirectView() + request = request_factory.get("/fake-url") + request.user = user - def test_get_success_url(self): - # Expect: '/users/testuser/', as that is the default username for - # self.make_user() - self.assertEqual(self.view.get_success_url(), "/users/testuser/") + view.request = request - def test_get_object(self): - # Expect: self.user, as that is the request's user object - self.assertEqual(self.view.get_object(), self.user) + assert view.get_redirect_url() == f"/users/{user.username}/" diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/urls.py b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/urls.py index fef93ad58..2502a0c0c 100644 --- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/urls.py +++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/urls.py @@ -1,15 +1,16 @@ from django.urls import path -from . import views +from {{ cookiecutter.project_slug }}.users.views import ( + user_list_view, + user_redirect_view, + user_update_view, + user_detail_view, +) app_name = "users" urlpatterns = [ - path("", view=views.UserListView.as_view(), name="list"), - path("~redirect/", view=views.UserRedirectView.as_view(), name="redirect"), - path("~update/", view=views.UserUpdateView.as_view(), name="update"), - path( - "/", - view=views.UserDetailView.as_view(), - name="detail", - ), + path("", view=user_list_view, name="list"), + path("~redirect/", view=user_redirect_view, name="redirect"), + path("~update/", view=user_update_view, name="update"), + path("/", view=user_detail_view, name="detail"), ] diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/views.py b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/views.py index a9038b71e..35e26e94f 100644 --- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/views.py +++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/views.py @@ -1,43 +1,52 @@ +from django.contrib.auth import get_user_model from django.contrib.auth.mixins import LoginRequiredMixin from django.urls import reverse from django.views.generic import DetailView, ListView, RedirectView, UpdateView -from .models import User +User = get_user_model() class UserDetailView(LoginRequiredMixin, DetailView): + model = User - # These next two lines tell the view to index lookups by username slug_field = "username" slug_url_kwarg = "username" +user_detail_view = UserDetailView.as_view() + + +class UserListView(LoginRequiredMixin, ListView): + + model = User + slug_field = "username" + slug_url_kwarg = "username" + + +user_list_view = UserListView.as_view() + + +class UserUpdateView(LoginRequiredMixin, UpdateView): + + model = User + fields = ["name"] + + def get_success_url(self): + return reverse("users:detail", kwargs={"username": self.request.user.username}) + + def get_object(self): + return User.objects.get(username=self.request.user.username) + + +user_update_view = UserUpdateView.as_view() + + class UserRedirectView(LoginRequiredMixin, RedirectView): + permanent = False def get_redirect_url(self): return reverse("users:detail", kwargs={"username": self.request.user.username}) -class UserUpdateView(LoginRequiredMixin, UpdateView): - - fields = ["name"] - - # we already imported User in the view code above, remember? - model = User - - # send the user back to their own page after a successful update - - def get_success_url(self): - return reverse("users:detail", kwargs={"username": self.request.user.username}) - - def get_object(self): - # Only get the User record for the user making the request - return User.objects.get(username=self.request.user.username) - - -class UserListView(LoginRequiredMixin, ListView): - model = User - # These next two lines tell the view to index lookups by username - slug_field = "username" - slug_url_kwarg = "username" +user_redirect_view = UserRedirectView.as_view()