Często powtarzającą się funkcjonalnością każdej aplikacji jest tworzenie i zarządzanie kontami użytkowników. Django w tej kwestii dostarcza gotowe rozwiązanie, jednak rozszerzenie standardowego użytkownika jest skomplikowane. Dodatkowo, Django używa pola username
do logowania, co zmusza użytkownika do pamiętania kolejnego loginu. W tym poście pokażę jak wykorzystać standardowe mechanizmy Django do stworzenia własnego modelu użytkownika, z dodatkowymi polami. Na koniec pokażę jak zamienić standardowe pole username
na email
w procesie tworzenia konta i logowania.
Do celów tego posta stworzyłem repozytorium django_auth, dostępne tutaj: https://github.com/jacoor/django_auth/tree/0.0.1. Kod też można zbudować samemu poprzez:
- Stworzenie nowego projektu opartego na Django.
- Prześledzenie tego artykułu i modyfikowanie zgodnie z moimi krokami.
Podstawowe ustawienia
Poniższe ustawienia są wymagane w pliku settings.py
. Przy nowym projekcie te ustawienia są automatyczne.
INSTALLED_APPS = [
# …
'django.contrib.auth', # Core authentication framework and its default models.
'django.contrib.contenttypes', # Django content type system (allows permissions to be associated with models).
# …
MIDDLEWARE = [
# …
'django.contrib.sessions.middleware.SessionMiddleware', # Manages sessions across requests
# …
'django.contrib.auth.middleware.AuthenticationMiddleware', # Associates users with requests using sessions.
# …
Django zapewnia prawie wszystko do tworzenia stron z obsługą logowania, wylogowywania i zarządzania hasłami „od razu po wyjęciu z pudełka”. Obejmuje to narzędzie do mapowania adresów URL, widoki i formularze ale szablony (html) trzeba zrobić samemu.
# Add Django site authentication urls (for login, logout, password management)
urlpatterns += [
path('accounts/', include('django.contrib.auth.urls')),
]
źródło: https://docs.djangoproject.com/en/4.2/topics/auth/default/#using-the-views
Dodanie powyższego kodu do urls.py
pozwoli na wykorzystanie standardowych Djangowych funkcji do obsługi użytkowników i stworzy widoki pod następującymi ścieżkami:
accounts/ login/ [name='login']
accounts/ logout/ [name='logout']
accounts/ password_change/ [name='password_change']
accounts/ password_change/done/ [name='password_change_done']
accounts/ password_reset/ [name='password_reset']
accounts/ password_reset/done/ [name='password_reset_done']
accounts/ reset/<uidb64>/<token>/ [name='password_reset_confirm']
accounts/ reset/done/ [name='password_reset_complete']
źródło: https://docs.djangoproject.com/en/4.2/topics/auth/default/#using-the-views
Wchodząc na strone główną projektu otrzymamy błąd, który pokazuje jakie mamy dostępne adresy - są zgodne z tymi powyżej wraz z dodanym adresem panelu administracyjnego.
Próba wejścia na ścieżkę z accounts
kończy się błędem:
Tak, to jest w porządku. Django dostarcza nam widoki i urle, ale nie szablony. Te trzeba napisać sobie samemu.
Interesująca informacja jest dalej w tym błędzie ze screena powyżej:
Template-loader postmortem
Django tried loading these templates, in this order:
Using engine django:
django.template.loaders.app_directories.Loader: /Users/jacek/Workspace/akademiaIT/django_auth/.venv/lib/python3.11/site-packages/django/contrib/admin/templates/registration/login.html (Source does not exist)
django.template.loaders.app_directories.Loader: /Users/jacek/Workspace/akademiaIT/django_auth/.venv/lib/python3.11/site-packages/django/contrib/auth/templates/registration/login.html (Source does not exist)
Django wskazuje, gdzie szuka szablonów. Wskazuje wyraźnie, że szuka ich wewnątrz Django: "/.venv/lib/python3.11/site-packages/django/...", gdzie ich po prostu nie ma.
Na tę chwilę nasze repozytorium wygląda tak:
Nie ma żadnej aplikacji, są tylko podstawowe ustawienia - katalog django_auth
, w którym jest settings.py
i cała reszta konfiguracji.
Żeby ułatwić dalszą pracę z kontami użytkowników, stworzę aplikacje "accounts" ./manage.py startapp accounts
.
Oczywiście aplikacje trzeba dodać do settings.py
w sekcji INSTALLED_APPS
.
Teraz możemy dodać sobie szablony. Pierwszy jest formularz logowania pod adresem accounts/templates/registration/login
.
<p>Formularz logowania</p>
<form action="." method="POST">
{% csrf_token %}
{{form.as_p}}
<input type="submit" value="zaloguj" />
</form>
Jest to najprostszy możliwy formularz logowania. Problem w tym, że jest po angielsku, więc trzeba zmienić język django. Wystarczy w settings.py
ustawić LANGUAGE_CODE
na "pl" dzięki czemu zobaczymy wszystko w ojczystym języku.
Jak widać formularz również poprawnie obsługuje błędy.
Wszystkie widoki dotyczące rejestracji, zmiany haseł itp dla użytkowników można obsłużyć w ten sam, opisany wyżej sposób. Pełna lista widoków tutaj: https://docs.djangoproject.com/en/4.2/topics/auth/default/#all-authentication-views
Ktoś zauważył czego brakuje?
Nie mamy rejestracji użytkownika!
Rejestracja użytkownika
Rejestracje użytkownika trzeba zrobić samemu. Django nie ma pojęcia, jakich pól wymagamy do rejestracji.
Potrzebne sa poniższe zmiany.
Tworzymy formularz rejestracji użytkownika w pliku accounts/forms.py
.
Uwaga: W tej wersji korzystamy ze standardowego modelu użytkownika Django czyli django.contrib.auth.models.User
, stąd wykorzystanie pola username
. Jak go modyfikować, pokażę w kolejnych krokach.
# accounts/forms.py
from django import forms
from django.contrib.auth.models import User
class RegistrationForm(forms.ModelForm):
password = forms.CharField(widget=forms.PasswordInput)
confirm_password = forms.CharField(widget=forms.PasswordInput)
class Meta:
model = User
fields = ["username", "email", "password"]
def clean_confirm_password(self):
password = self.cleaned_data.get("password")
confirm_password = self.cleaned_data.get("confirm_password")
if password != confirm_password:
raise forms.ValidationError("Hasła nie pasują do siebie.")
return confirm_password
def save(self, commit=True):
user = super(RegistrationForm, self).save(commit=False)
user.is_active = (
True # Ustawienie is_active na True, inaczej nie działa logowanie
)
if commit:
user.save()
return user
Na szczególną uwagę zasługje metoda save
- ustawia użytkownika jako aktywnego. Bez tego nie bedzie mógł się zalogować.
Tworzymy widok w accounts/views.py
.
# accounts/views.py
from django.contrib.auth import login, authenticate
from django.urls import reverse_lazy
from django.views.generic.edit import CreateView
from .forms import RegistrationForm
class RegistrationView(CreateView):
template_name = "registration/registration.html"
form_class = RegistrationForm
success_url = reverse_lazy("") # Przekierowanie po udanej rejestracji. Tak, puste. Ćwiczenie dla Was.
"""
Widok oprócz rejestrowania użytkownika od razu go loguje.
Gdyby pominąć logowanie, całą funkcje "form_valid" można usunąć.
"""
def form_valid(self, form):
valid = super().form_valid(form)
user = form.save()
username = form.cleaned_data.get("username")
raw_password = form.cleaned_data.get("password")
user = authenticate(username=username, password=raw_password)
login(self.request, user)
return valid
Widok jest oparty na ClassBasedViews, CreateView - dzięki temu nie musimy pisać za dużo kodu. Widoki klasowe opisywałem już wcześniej na blogu.
Żeby wszystko działało, trzeba wskazać Django nowy widok. Dokonuje się tego poprzez dodanie ścieżki w pliku accounts/urls.py
.
# accounts/urls.py
from django.urls import path
from .views import RegistrationView
urlpatterns = [
path("register/", RegistrationView.as_view(), name="registration"),
]
Trzeba też zmodyfikować główny plik urls.py
poprzez dodanie informacji o pliku accounts/urls.py
-> path("accounts/", include("accounts.urls")),
.
# django_auth/urls.py
"""
URL configuration for django_auth project.
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/4.2/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path("admin/", admin.site.urls),
path("accounts/", include("django.contrib.auth.urls")),
path("accounts/", include("accounts.urls")),
]
Czy zauważyliście ciekawą rzecz? Otóż są dwie ścieżki accounts/
. Django czyta ścieżki jedna po drugiej, dodaje je do siebie, a jeśli jest kilka takich samych, to ostatnia nadpisuje pozostałe.
Pozostaje stworzyć szablon html. Właściwie, można by wykorzystać ten sam co w logowaniu, bo korzystamy z {{ form.as_p}}
, czyli oddajemy pełną kontrolę nad formularzem do widoku. Jednak czyściej będzie stworzyć sobie oddzielny widok.
{# accounts/templates/registration/registration.html #}
<p>Formularz rejestracji</p>
<form action="." method="POST">
{% csrf_token %}
{{form.as_p}}
<input type="submit" value="zarejestruj" />
</form>
W efekcie stworzyliśmy stronę rejestracji użytkownika.
Po poprawnej rejestracji Django przekieruje nowego użytkownika do nieistniejącej strony success_url = reverse_lazy("")
- tutaj zostawiam zadanie dla Was do uzupełnienia.
Jak wymusić logowanie po adresie email
zamiast username
?
Domyślny model użytkownika w Django nie ma ograniczenia "unique" na polu email
. Pozwala to na wielokrotną rejestracje z tym samym adresem email, co z kolei może być źródłem wielu problemów, chociażby przy odzyskiwaniu hasła:
Do którego konta chcesz zresetować hasło skoro wszystkie są pod jacek@akademiait.com.pl?
Rozwiązaniem tego problemu jest zmiana standardowego pola username
na email
. Uwaga: zmiana ta jest trudna, w przypadku kiedy już mamy jakiekolwiek dane w bazie. Bez obaw, więcej detali za chwile.
Żeby skorzystać z pola email
do logowania potrzebujemy dodać własny model użytkownika oraz zmodyfikować UserManager
.
# accounts/models.py
from django.db import models
from django.contrib.auth.models import AbstractUser
from django.utils.translation import gettext_lazy as _
from django.contrib.auth.base_user import BaseUserManager
class UserManager(BaseUserManager):
use_in_migrations = True
"""
Klasycznie django przy tworzeniu użytkowników i super użytkowników oczekuje pola `username`
które zostało usunięte z modelu poprzez `username = None`.
Ta klasa jest dostosowana do zastosowania pola "email" do logowania.
"""
def _create_user(self, email, password, **extra_fields):
if not email:
raise ValueError("Users require an email field")
email = self.normalize_email(email)
user = self.model(email=email, **extra_fields)
user.set_password(password)
user.save(using=self._db)
return user
def create_user(self, email, password=None, **extra_fields):
extra_fields.setdefault("is_staff", False)
extra_fields.setdefault("is_superuser", False)
return self._create_user(email, password, **extra_fields)
def create_superuser(self, email, password, **extra_fields):
extra_fields.setdefault("is_staff", True)
extra_fields.setdefault("is_superuser", True)
if extra_fields.get("is_staff") is not True:
raise ValueError("Superuser must have is_staff=True.")
if extra_fields.get("is_superuser") is not True:
raise ValueError("Superuser must have is_superuser=True.")
return self._create_user(email, password, **extra_fields)
class User(
AbstractUser
): # Dziedziczymy z AbstractUser - standardowego użytkownika Django
USERNAME_FIELD = "email"
# domyślnie pole username jest wymagane, więc trzeba to wyczyścić
REQUIRED_FIELDS = []
# wskazujemy zdefiniowany powyżej manager użytkowników
objects = UserManager()
# usuwam pole z modelu
username = None
# no i wymuszam email unikalny
email = models.EmailField(unique=True)
Modyfikujemy settings.py
poprzez dodanie: AUTH_USER_MODEL = 'accounts.User'
, czyli wskazujemy właśnie stworzony model Django.
Oczywiście trzeba stworzyć migracje ./manage.py makemigrations
i ją wykonać ./manage.py migrate
.
OOUPS!
Kłopot. Migracja padła. To jest to, o czym pisałem wcześniej - podmiana modelu użytkownika "w locie" jest trudna. Trzeba wyeksportować dane z bazy danych, usunąć baze, przeprowadzić migracje, zaktualizować kod oraz zaimportować ponownie dane. Ta procedura przekracza znacząco zakres tego posta. Dzieje się tak dlatego, że model użytkownika jest potrzebny w momencie tworzenia bazy danych. Znalazłem opis jak zmienić model użytkownika w istniejącym projekcie tutaj: https://testdriven.io/blog/django-custom-user-model-migration/. Nie testowałem tej procedury.
My skorzystamy z tego, że projekt jest oparty na bazie SQLite. Ta baza danych jest oparta na pliku db.sqlite3
który znajduje się w naszej aplikacji. Usuwam ten plik - w końcu nie mam w aplikacji żadnych danych. Powoduje to, że aplikacja straciła całą bazę danych, wszystkich użytkowników itp. To też w sumie rozwiązało nasz problem z migracją - startujemy od zera.
Teraz ./manage.py migrate
przechodzi.
Dodatkowo trzeba usunąć pole username
z pliku: forms.py
-> RegistrationForm.Meta.fields
oraz zamienić importy:
from django.contrib.auth.models import User
na from accounts.models import User
.
Ostatnia zmiana to dodanie user.set_password(self.cleaned_data["password"])
do metody save
w RegistrationForm
:
# accounts/forms.py RegistrationForm
def save(self, commit=True):
user = super(RegistrationForm, self).save(commit=False)
user.is_active = (
True # Ustawienie is_active na True, inaczej nie działa logowanie
)
"""
Ustawiam hasło korzystając z metody set_password. W przeciwnym wypadku Django
zapisze hasło jako zwykły tekst, bez zakodowania, co z kolei skutecznie
zablokuje możliwość logowania.
"""
user.set_password(self.cleaned_data["password"])
if commit:
user.save()
return user
I proszę, rejestracja działa, logowanie też! Admin również już wymaga pola email.
Dodatkowo, niejako w gratisie, możemy dodać dowolne pola do naszego modelu użytkownika - adres, numer pesel, date urodzenia, zgodę marketingową itd..
Teraz jeszcze dobrze by było widzieć naszych użytkowników w panelu administracyjnym, poprzez modyfikacje accounts/admin.py
# accounts/admin.py
from django.contrib import admin
from .models import User
admin.site.register(User)
To taka najprostrza wersja. Problem w tym:
Pole hasło jest wyświetlane w zakodowanej, edytowalnej wersji. Może to powodować przypadkową zmianę hasła podczas edycji innych danych użytkownika. Trzeba "przypomnieć" Django, że to pole zawiera hasło i przez to wymaga specjalnego traktowania. W tym celu trzeba stworzyć swoją klasę CustomUserAdmin
, która będzie obsługiwała wyświetlanie użytkowników w panelu administracyjnym.
# accounts/admin.py
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from .models import User
@admin.register(User)
class CustomUserAdmin(UserAdmin):
list_display = UserAdmin.list_display[1:]
ordering = ("email",)
fieldsets = ()
add_fieldsets = (
(
None,
{
"classes": ("wide",),
"fields": (
"email",
"first_name",
"last_name",
"password1",
"password2",
),
},
),
)
W powyższej wersji mamy już poprawnie działającego admina, poprawnie wyświetlającego niezbędne pola. Działa dodawanie użytkowników. Korzystajcie. Kod źródłowy jest dostępny tutaj: https://github.com/jacoor/django_auth/tree/0.0.1
Podsumowanie
W tym dość długim poście pokazałem jak stworzyć system logowania do serwisu opartego na Django.
Pokazałem, z jakich standardowych widoków korzysta Django i jak je wykorzystać w swoim projekcie. Zrobiłem to poprzez stworzenie formularzy rejestracji i logowania dostępnych odpowiednio na /accounts/register/
i /accounts/login/
.
Dodatkowo pokazałem jak zamienić standardowe logowanie po polu username
na bardziej przyjazne pole email
.
Nie zaimplementowałem odzyskiwania hasła ani jego zmiany, poza panelem administracyjnym. To już zadanie dla Was. Albo, może pojawi się w kolejnym poście?
Kod można pobrać z GitHuba: https://github.com/jacoor/django_auth/tree/0.0.1. Możecie z tego kodu korzystać we własnych projektach.
Zapraszam do zadawania pytań przez formularz kontaktowy. Pamiętaj, że jeśli potrzebujesz wsparcia, możesz napisać do mnie - pomogę.
Spodobał Ci się post?
Podziel się nim!
Masz uwagi do posta, chcesz porozmawiać, szukasz pomocy z Pythonem i Django? Napisz do mnie!