Как изменить данные пользователя django

In this final part of the series, we are going to create and display a form where users can update...

Cover image for Django - Update User Profile

Hana Belay

In this final part of the series, we are going to create and display a form where users can update their profile. This is what the profile page will look like by the end of this tutorial.

Profile Page

To create the forms, we are going to use ModelForm. ModelForm allows us to create forms that interact with a specific model inside the database.

forms.py

from django import forms

from django.contrib.auth.models import User
from .models import Profile


class UpdateUserForm(forms.ModelForm):
    username = forms.CharField(max_length=100,
                               required=True,
                               widget=forms.TextInput(attrs={'class': 'form-control'}))
    email = forms.EmailField(required=True,
                             widget=forms.TextInput(attrs={'class': 'form-control'}))

    class Meta:
        model = User
        fields = ['username', 'email']


class UpdateProfileForm(forms.ModelForm):
    avatar = forms.ImageField(widget=forms.FileInput(attrs={'class': 'form-control-file'}))
    bio = forms.CharField(widget=forms.Textarea(attrs={'class': 'form-control', 'rows': 5}))

    class Meta:
        model = Profile
        fields = ['avatar', 'bio']

Enter fullscreen mode

Exit fullscreen mode

  • UpdateUserForm interacts with the user model to let users update their username and email.
  • UpdateProfileForm interacts with the profile model to let users update their profile.
  • We gave the fields some bootstrap as well.

Now let’s update the view to add the forms we just created.

views.py

from django.shortcuts import render, redirect
from django.contrib import messages
from django.contrib.auth.decorators import login_required

from .forms import UpdateUserForm, UpdateProfileForm


@login_required
def profile(request):
    if request.method == 'POST':
        user_form = UpdateUserForm(request.POST, instance=request.user)
        profile_form = UpdateProfileForm(request.POST, request.FILES, instance=request.user.profile)

        if user_form.is_valid() and profile_form.is_valid():
            user_form.save()
            profile_form.save()
            messages.success(request, 'Your profile is updated successfully')
            return redirect(to='users-profile')
    else:
        user_form = UpdateUserForm(instance=request.user)
        profile_form = UpdateProfileForm(instance=request.user.profile)

    return render(request, 'users/profile.html', {'user_form': user_form, 'profile_form': profile_form})

Enter fullscreen mode

Exit fullscreen mode

  • You should be familiar with most of the things we did in the above code but to recap, basically what it does is import the
    required forms and create instances of those forms depending on whether the request is get or post.
  • If the form is submitted (request is post), we need to pass in the post data to the forms. But for the profile form there is a file/image data coming in with the request. This file data is placed in request.FILES so we need to pass in that too.
  • Then it populates the form fields with the current information of the logged in user i.e. The user form expects an instance of a user since it’s working with the User model so we say instance=request.user while for the profile form we pass in an instance of the profile model by saying instance=request.user.profile.

Finally let’s update the template to display the forms

profile.html

{% extends "users/base.html" %}
{% block title %}Profile Page{% endblock title %}
{% block content %}
    <div class="row my-3 p-3">
        <img class="rounded-circle account-img" src="{{ user.profile.avatar.url }} " style="cursor: pointer;"/>
    </div>
    {% if user_form.errors %}
        <div class="alert alert-danger alert-dismissible" role="alert">
            <div id="form_errors">
                {% for key, value in user_form.errors.items %}
                    <strong>{{ value }}</strong>
                {% endfor %}
            </div>
            <button type="button" class="close" data-dismiss="alert" aria-label="Close">
                <span aria-hidden="true">&times;</span>
            </button>
        </div>
    {% endif %}
    <div class="form-content">
        <form method="post" enctype="multipart/form-data">
            {% csrf_token %}
            <div class="form-row">
                <div class="col-md-5">
                    <div class="form-group">
                        <label class="small mb-1">Username:</label>
                            {{ user_form.username }}
                        <label class="small mb-1">Email:</label>
                            {{ user_form.email }}
                    </div>
                    <div class="form-group">
                        <a href="#">Change Password</a>
                        <hr>
                        <label class="small mb-1">Change Avatar:</label>
                        {{ profile_form.avatar }}
                    </div>
                    <label class="small mb-1">Bio:</label> {{ profile_form.bio }}
                </div>
            </div>
            <br><br>
            <button type="submit" class="btn btn-dark btn-lg">Save Changes</button>
            <button type="reset" class="btn btn-dark btn-lg">Reset</button>
        </form>
    </div>
{% endblock content %}

Enter fullscreen mode

Exit fullscreen mode

  • Notice the dead link for Change Password. We will update it later at the end of this tutorial when we add change password functionality into our app.

Resizing Images In Django

Having large images saved only to show a scaled/smaller version of it on the profile page might cause our app to run slow. We can mitigate this problem by using pillow to resize the large image and override it with the resized/smaller image.

Now, in order to save this resized image we need to override the save() method which is a method that exists for all models and it is used to save an instance of the model.

Why do we need to override this method?

It is because we need a customized saving behavior.

Open models.py and add the following inside the Profile model.

models.py

from PIL import Image

# resizing images
def save(self, *args, **kwargs):
    super().save()

    img = Image.open(self.avatar.path)

    if img.height > 100 or img.width > 100:
        new_img = (100, 100)
        img.thumbnail(new_img)
        img.save(self.avatar.path)

Enter fullscreen mode

Exit fullscreen mode

What the above code does is:

  • Save the uploaded picture
  • Open the image and check if it has a dimension larger than 100 pixels. If it has, resize the image and save it in that same path it was originally saved (overriding the original large image). Therefore, when the app is running, the browser is no longer getting that large image to display to the user.

Change Password

Change Password

Usually in the profile page, users should be able to change their password. This is going to be really simple because we have discussed how users can reset their password if they forget it, and the process is going to be pretty much similar.

Django has a view called PasswordChangeView which allows users to change their password. Let’s create our own view which will extend from this and override some of the class’s attributes.

views.py

from django.urls import reverse_lazy
from django.contrib.auth.views import PasswordChangeView
from django.contrib.messages.views import SuccessMessageMixin


class ChangePasswordView(SuccessMessageMixin, PasswordChangeView):
    template_name = 'users/change_password.html'
    success_message = "Successfully Changed Your Password"
    success_url = reverse_lazy('users-home')

Enter fullscreen mode

Exit fullscreen mode

  • Since this is similar to what we did here when resetting password, am not going to explain it here, but feel free to ask if there is any confusion.

Now go to the project’s urls.py and create the route for this view.

user_management/urls.py

from django.urls import path
from users.views import ChangePasswordView

urlpatterns = [
    path('password-change/', ChangePasswordView.as_view(), name='password_change'),
]

Enter fullscreen mode

Exit fullscreen mode

Finally create the template for the view.

change_password.html

{% extends "users/base.html" %}
{% block content %}
    <div class="form-content my-3 p-3">
        <div class="container">
            <div class="row justify-content-center">
                <div class="col-lg-5">
                        <div class="card shadow-lg border-0 rounded-lg mt-0 mb-3">
                            <div class="card-header justify-content-center">
                              <h3 class="font-weight-light my-4 text-center">Change Your Password</h3>
                            </div>
                            {% if form.errors %}
                                <div class="alert alert-danger alert-dismissible" role="alert">
                                    <div id="form_errors">
                                        {% for key, value in form.errors.items %}
                                            <strong>{{ value }}</strong>
                                        {% endfor %}
                                    </div>
                                    <button type="button" class="close" data-dismiss="alert" aria-label="Close">
                                        <span aria-hidden="true">&times;</span>
                                    </button>
                                </div>
                            {% endif %}

                            <div class="card-body">
                                <form method="POST">
                                    {% csrf_token %}
                                    <div class="form-row">
                                        <div class="col-md-10 offset-md-1">
                                            <div class="form-group">
                                                <label class="small mb-1" for="id_old_password">Old Password</label>
                                                <input type="password" name="old_password" autocomplete="new-password"
                                                       class="form-control" required id="id_old_password"
                                                       placeholder="Enter Old Password"/>
                                                <span>
                                                </span>
                                            </div>
                                        </div>
                                    </div>
                                    <div class="form-row">
                                        <div class="col-md-10 offset-md-1">
                                            <div class="form-group">
                                                <label class="small mb-1" for="id_new_password1">New Password</label>
                                                <input type="password" name="new_password1" autocomplete="new-password"
                                                       class="form-control" required id="id_new_password1"
                                                       placeholder="Enter New Password"/>
                                                <span>
                                                </span>
                                            </div>
                                        </div>
                                    </div>
                                    <div class="form-row">
                                        <div class="col-md-10 offset-md-1">
                                            <div class="form-group">
                                                <label class="small mb-1" for="id_new_password2">New Password Confirmation</label>
                                                <input type="password" name="new_password2" autocomplete="new-password"
                                                       required id="id_new_password2" class="form-control"
                                                       placeholder="Confirm New Password"/>
                                            </div>
                                        </div>
                                    </div>
                                    <div class="form-row">
                                        <div class="col-md-10 offset-md-1">
                                            <div class="form-group mt-0 mb-1">
                                                <button type="submit" class="col-md-12 btn btn-dark" id="reset">Update Password</button>
                                            </div>
                                        </div>
                                    </div>
                                </form>
                            </div>
                        </div>
                </div>
            </div>
        </div>
    </div>
{% endblock content %}

Enter fullscreen mode

Exit fullscreen mode

  • In the profile page update the link to Change Password like this <a href="{% url 'password_change' %}">Change Password</a>

Alright,

  • Start the development server and run the usual command python manage.py runserver in your terminal.
  • Go to localhost, play around with the profile page and admin panel.

That’s it for the series. I’m glad I was able to write this here. I thank you all for your time and feedback!

You can find the finished app in github

Top Heroku Alternatives (For Free!)

Recently Heroku shut down free Heroku Dynos, free Heroku Postgres, and free Heroku Data for Redis on November 28th, 2022. So Meshv Patel put together some free alternatives in this classic DEV post.

Read next


balastrong profile image

100% Code Coverage is a Lie 🎯

Leonardo Montini — Feb 9


kuldeeptarapara profile image

How to Use Deep Linking in Flutter?

Kuldeep Tarapara — Feb 9


bytebodger profile image

Optimizing Functional React Components

Adam Nathaniel Davis — Feb 9


mayank091193 profile image

Deploy web application built using Quasar framework and Python Flask framework on AWS EC2 instance, using PM2 & Nginx web server

Mayank Patel — Feb 9

Once unpublished, all posts by earthcomfy will become hidden and only accessible to themselves.

If earthcomfy is not suspended, they can still re-publish their posts from their dashboard.

Note:

Once unpublished, this post will become invisible to the public and only accessible to Hana Belay.

They can still re-publish the post if they are not suspended.

Thanks for keeping DEV Community 👩‍💻👨‍💻 safe. Here is what you can do to flag earthcomfy:

Make all posts by earthcomfy less visible

earthcomfy consistently posts content that violates DEV Community 👩‍💻👨‍💻’s
code of conduct because it is harassing, offensive or spammy.

I propse a slightly improved version of André’s solution as it breaks the list view in /admin/auth/user/:

from django.contrib import admin
from member.models import UserProfile
from django.contrib.auth.models import User
from django.contrib.auth.admin import UserAdmin as AuthUserAdmin

class UserProfileInline(admin.StackedInline):
 model = UserProfile
 max_num = 1
 can_delete = False

class UserAdmin(AuthUserAdmin):
 inlines = [UserProfileInline]

# unregister old user admin
admin.site.unregister(User)
# register new user admin
admin.site.register(User, UserAdmin)

answered Jan 23, 2011 at 12:26

Robert Lacroix's user avatar

Robert LacroixRobert Lacroix

4291 gold badge4 silver badges3 bronze badges

2

I propose another improvement to Robert’s solution:

from django.contrib import admin
from member.models import UserProfile
from django.contrib.auth.models import User
from django.contrib.auth.admin import UserAdmin as AuthUserAdmin

class UserProfileInline(admin.StackedInline):
   model = UserProfile
   max_num = 1
   can_delete = False

class UserAdmin(AuthUserAdmin):
   def add_view(self, *args, **kwargs):
      self.inlines = []
      return super(UserAdmin, self).add_view(*args, **kwargs)

   def change_view(self, *args, **kwargs):
      self.inlines = [UserProfileInline]
      return super(UserAdmin, self).change_view(*args, **kwargs)

# unregister old user admin
admin.site.unregister(User)
# register new user admin
admin.site.register(User, UserAdmin)

Without this change to UserAdmin, the custom UserProfileInline section will show up on the «add user» screen, which is just supposed to ask for the username and password. And if you change any of the profile data on that screen (away from the defaults) before you save, you’ll get a «duplicate key» database error.

answered Apr 26, 2012 at 23:28

abjennings's user avatar

abjenningsabjennings

2,5733 gold badges21 silver badges22 bronze badges

2

Well, it turns out that this is quite easy, once you know how to do it. This is my myapp/accounts/admin.py:

from django.contrib import admin
from myapp.accounts.models import UserProfile
from django.contrib.auth.models import User

class UserProfileInline(admin.StackedInline):
    model = UserProfile
    max_num = 1
    can_delete = False

class AccountsUserAdmin(admin.UserAdmin):
    inlines = [UserProfileInline]

# unregister old user admin
admin.site.unregister(User)
# register new user admin that includes a UserProfile
admin.site.register(User, AccountsUserAdmin)

The default admin.UserAdmin ModelAdmin class for users is unregistered and a new one specifying an inline UserProfile is registered in its place. Just thought I should share.

Daniel Sokolowski's user avatar

answered Aug 3, 2010 at 20:49

André Laszlo's user avatar

André LaszloAndré Laszlo

14.9k3 gold badges65 silver badges77 bronze badges

2

You have to consider the add and change form. OTherwise you will get a user cannot be None error when trying to create a user. The following has been tested and works in 1.3:

class TeamInline(admin.StackedInline):
  model = Team
  fk_name = 'user'
  max_num = 1
  can_delete = False

class TeamUserAdmin(UserAdmin):
  list_display = ('username', 'email', 'company', 'expertise', 'contact_email', 'contact_phone', 'twitter', 'facebook', 'last_login_short', 'options')
  list_select_related = True

  def add_view(self, *args, **kwargs):
    self.inline_instances = []
    return super(TeamUserAdmin, self).add_view(*args, **kwargs)

  def change_view(self, *args, **kwargs):
    self.inline_instances.append(TeamInline(self.model, self.admin_site))
    return super(TeamUserAdmin, self).change_view(*args, **kwargs)

answered Sep 27, 2012 at 2:38

Paul Kenjora's user avatar

Paul KenjoraPaul Kenjora

1,87418 silver badges19 bronze badges

Quick Summary:-

In this blog, we will learn how to edit User profile both Django User and custom User fields. And the custom User model is UserProfile.

Prerequisites:-

  1. I have used python version 3.6
  2. Django version 2.0.2
  3. Operating System Ubuntu 16.04
  4. MySQL
  5. IDE — pycharm-community-2018.2.5

Let’s start to create Django project:- 

Now, create accounts app:-

And run the project-

If we navigate this Url http://127.0.0.1:8000. We’ll see Django welcome page.

Add «accounts» at the bottom of «INSTALLED_APPS». And add the «AUTH_PROFILE_MODULE» config at the bottom of the entire file.

We will add «accounts» app in settings.py and use the «AUTH_USER_MODEL» config to tell Django to use our another model. And our another model will UserProfile.

Update database configuration in settings.py-

where ‘custom’ is database name. And we will create ‘custom’ database through terminal.

In modles.py, we will add UserProfile model.

UserProfile model, that means it is a entity class. And created «accounts_userprofile» table in «custom» database. Where all entry of UserProfile will be store.

In forms.py:-

Django form is a nice thing that we can create a ModelForm which will save the result of the form to the model exactly what we want to do.

In views.py:-

In views.py, we can write our custom view for our HTML templates and will use the ORM to get the data from the database.

In urls.py:-

In the accounts app, we make templates folder and within the templates folder create base.html:-

In templates folder make another accounts folder.
Now we create edit_profile.html and view_profile.html file in accounts folder.

{% block head %}

Profile

{%endblock%}

{%block body%}

Profile

Username: {{ user }} First name: {{ user.first_name }} Last name: {{ user.last_name }} Email: {{ user.email }} Description: {{ user.userprofile.description }} Phone: {{ user.userprofile.phoneNumber }} City: {{ user.userprofile.city }} Website: {{ user.userprofile.website }} {%endblock%}

In urls.py:-

Now we can run make migrations and migrate commands to create a new database that uses the custom user model.

Conclusion:-
                   In this article, we have covered how to edit User profile both Django User and custom User fields.

Reference:-
              External links to the source code-
              1)- https://www.youtube.com/watch?v=Fc2O3_2kax8&list=PLw02n0FEB3E3VSHjyYMcFadtQORvl1Ssj

Встроенная система аутентификации Django очень хороша и безопасна. Ее можно использовать, не меняя ни строчки кода, что экономит силы на разработку и тестирование. Стандартной функциональности хватает для большинства случаев.

Но иногда случается, что в нее нужно внести некоторые изменения, чтобы она подходила вашему веб-приложению: возможно, вам необходимо сохранить дополнительные данные пользователя, например, краткое описание или местоположение.

В этой статье мы сравним различные способы расширения стандартной модели пользователя в Django.

Способы расширения существующей модели пользователей

Существует четыре разных способа расширения существующей модели пользователя, о которых пойдет речь в статье:

  • использование прокси-модели;
  • использование связи один-к-одному с пользовательской моделью;
  • создание модели пользователя с помощью расширения класса AbstractBaseUser;
  • создание модели пользователя с помощью расширения класса AbstractUser.

Для начала вам необходимо определиться, какой из способов подходит вам больше всего.

Использование прокси-модели

Прокси-модель — это модель, отнаследованная от существующей модели без создания новой таблицы в базе данных. Она используется для изменения поведения существующей модели, например, задания сортировки по умолчанию или добавления новых методов, не затрагивающих схему базы данных.

Когда следует использовать прокси-модель?

Прокси-модель используется для расширения существующей модели пользователя, когда в базе данных не нужно хранить дополнительную информацию, а нужно добавить базовой модели дополнительные методы или изменить ее Manager, управляющий запросами к базе данных.

Это то, что мне нужно! Перейти к коду!

Использование связи один-к-одному с пользовательской моделью

В этом случае создается обычная модель Django, у которой будет собственная таблица базы данных и которая будет одна-к-одной связана с существующей моделью пользователя через OneToOneField.

Когда следует использовать связи один-к-одному?

Связь один-к-одному используется, когда нужно хранить дополнительную информацию о существующей модели пользователей, которая не связана с процессом аутентификации. Такую модель обычно называют профилем пользователя.

Это то, что мне нужно! Перейти к коду!

Создание модели пользователя через расширение AbstractBaseUser

Это совершенно новая модель пользователя, которая наследуется от AbstractBaseUser. Ее грамотная интеграция требует дополнительных усилий и обновления некоторых связей через settings.py. При выборе этого варианта настоятельно рекомендуется провести все манипуляции с моделями до начала работы над проектом, так как это повлияет на всю схему базы данных. Использование этого способа в готовом проекте может вызвать проблемы при внедрении новой модели.

Когда следует использовать этот способ?

Использование пользовательской модели нужно тогда, когда приложение имеет особые требования к процессу аутентификации. Например, если вам нужно использовать адрес электронной почты вместо имени пользователя.

Это то, что мне нужно! Перейти к коду!

Создание модели пользователя через расширение AbstractUser

Этот способ также подразумевает создание новой модели пользователя, но которая наследуется уже от AbstractUser. Здесь актуальны все те же замечания, что и для пункта выше: необходимость дополнительных усилий для внедрения и обновления некоторых связей через settings.py, сложности при интеграции в готовый проект.

Когда следует использовать этот способ?

Используется только тогда, когда вас вполне устраивает, как работает аутентификация в Django и вы ничего не хотите в ней менять, но тем не менее, вам нужно добавить дополнительную информацию непосредственно в пользовательскую модель (User), причем без создания дополнительного класса (как в варианте с прокси-моделью).

Это то, что мне нужно! Перейти к коду!

Расширение модели пользователя через прокси-модель

Это один из самых простых способов расширить существующую модель пользователя. Используя этот способ, вы избежите сложностей, но будете сильно ограничены.

Вот как это можно сделать:

from django.contrib.auth.models import User
from .managers import PersonManager

class Person(User):
    objects = PersonManager()

    class Meta:
        proxy = True
        ordering = ('first_name', )

    def do_something(self):
        ...

В приведенном выше примере создается прокси-модель с именем Person. Тот факт, что она является прокси-моделью, указывается внутри класса Meta: proxy = True.

Сама прокси-модель в этом примере используется для переопределения сортировки по умолчанию, назначения нового Manager и определения нового метода do_something.

Отметим, что User.objects.all() и Person.objects.all() будут обращаться к одной и той же таблице базы данных. Единственное различие заключается в поведении, которое определяется для прокси-модели. Вот и все.

Расширение модели пользователя с помощью связи один-к-одному

Вероятно, этот метод и есть то, что вам нужно, потому что именно его используют чаще всего. Мы создадим новую модель для хранения дополнительной информации о пользователе.

Стоит понимать, что использование этой стратегии приводит к дополнительным запросам или объединениям для получения связанных данных. По сути, когда создается запрос к связанным данным, Django делает дополнительный запрос. Но этого можно избежать в большинстве случаев. Мы к этому вернемся чуть позже.

Обычно в Django такие модели называют Profile:

from django.db import models
from django.contrib.auth.models import User

class Profile(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    bio = models.TextField(max_length=500, blank=True)
    location = models.CharField(max_length=30, blank=True)
    birth_date = models.DateField(null=True, blank=True)

А теперь установим сигналы для Profile на автоматическое создание/обновление, когда мы создаем/обновляем стандартную модель пользователя (User):

from django.db import models
from django.contrib.auth.models import User
from django.db.models.signals import post_save
from django.dispatch import receiver

class Profile(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    bio = models.TextField(max_length=500, blank=True)
    location = models.CharField(max_length=30, blank=True)
    birth_date = models.DateField(null=True, blank=True)

@receiver(post_save, sender=User)
def create_user_profile(sender, instance, created, **kwargs):
    if created:
        Profile.objects.create(user=instance)

@receiver(post_save, sender=User)
def save_user_profile(sender, instance, **kwargs):
    instance.profile.save()

Как можно видеть, основная нагрузка — это добавление вызовов create_user_profile и save_user_profile всякий раз, когда происходит сохранение (в том числе создание) объекта. Этот вид сигнала называется post_save.

Дадим еще несколько поясняющих примеров. Можно использовать следующий код в шаблоне:

<h2>{{ user.get_full_name }}</h2>
<ul>
  <li>Имя пользователя: {{ user.username }}</li>
  <li>Местоположение: {{ user.profile.location }}</li>
  <li>Дата рождения: {{ user.profile.birth_date }}</li>
</ul>

Или такой внутри представления:

def update_profile(request, user_id):
    user = User.objects.get(pk=user_id)
    user.profile.bio = 'Lorem ipsum dolor sit amet, consectetur adipisicing elit...'
    user.save()

Вообще говоря, вам никогда не придется вызывать метод сохранения профиля. Все делается методами User.

Отдельно обсудим вопрос использования форм. Можно использовать более одной формы сразу. Как в этом примере:

# forms.py

class UserForm(forms.ModelForm):
    class Meta:
        model = User
        fields = ('first_name', 'last_name', 'email')

class ProfileForm(forms.ModelForm):
    class Meta:
        model = Profile
        fields = ('url', 'location', 'company')
# views.py

@login_required
@transaction.atomic
def update_profile(request):
    if request.method == 'POST':
        user_form = UserForm(request.POST, instance=request.user)
        profile_form = ProfileForm(request.POST, instance=request.user.profile)
        if user_form.is_valid() and profile_form.is_valid():
            user_form.save()
            profile_form.save()
            messages.success(request, _('Ваш профиль был успешно обновлен!'))
            return redirect('settings:profile')
        else:
            messages.error(request, _('Пожалуйста, исправьте ошибки.'))
    else:
        user_form = UserForm(instance=request.user)
        profile_form = ProfileForm(instance=request.user.profile)
    return render(request, 'profiles/profile.html', {
        'user_form': user_form,
        'profile_form': profile_form
    })
<!-- profile.html -->

<form method="post">
  {% csrf_token %}
  {{ user_form.as_p }}
  {{ profile_form.as_p }}
  <button type="submit">Сохранить изменения</button>
</form>

А теперь о дополнительных запросах к базе данных.

Django будет формировать запрос к базе данных только при доступе к одному из связанных свойств. Иногда это вызывает нежелательные эффекты, такие как запуск сотен или тысяч запросов. Этот эффект можно смягчить, используя метод select_related.

Зная заранее, что вам нужно будет получить доступ к связанным данным, вы можете предварительно выбрать их в одном запросе к базе данных:

users = User.objects.all().select_related('profile')

Расширение модели пользователя с помощью наследования AbstractBaseUser

Это самый сложный вариант, старайтесь избегать его любой ценой. Однако иногда это невозможно.

Допустим, нам нужно использовать адрес электронной почты в качестве логина и использование username совершенно бесполезно. Также у нас нет необходимости в использовании флага is_staff, поскольку мы не будем использовать админку Django.

В таком случае пользовательскую модель можно определить так:

from __future__ import unicode_literals

from django.db import models
from django.core.mail import send_mail
from django.contrib.auth.models import PermissionsMixin
from django.contrib.auth.base_user import AbstractBaseUser
from django.utils.translation import ugettext_lazy as _

from .managers import UserManager


class User(AbstractBaseUser, PermissionsMixin):
    email = models.EmailField(_('email'), unique=True)
    first_name = models.CharField(_('name'), max_length=30, blank=True)
    last_name = models.CharField(_('surname'), max_length=30, blank=True)
    date_joined = models.DateTimeField(_('registered'), auto_now_add=True)
    is_active = models.BooleanField(_('is_active'), default=True)
    avatar = models.ImageField(upload_to='avatars/', null=True, blank=True)

    objects = UserManager()

    USERNAME_FIELD = 'email'
    REQUIRED_FIELDS = []

    class Meta:
        verbose_name = _('user')
        verbose_name_plural = _('users')

    def get_full_name(self):
        '''
        Возвращает first_name и last_name с пробелом между ними.
        '''
        full_name = '%s %s' % (self.first_name, self.last_name)
        return full_name.strip()

    def get_short_name(self):
        '''
        Возвращает сокращенное имя пользователя.
        '''
        return self.first_name

    def email_user(self, subject, message, from_email=None, **kwargs):
        '''
        Отправляет электронное письмо этому пользователю.
        '''
        send_mail(subject, message, from_email, [self.email], **kwargs)

В коде все сделано так, чтобы сохранить новую пользовательскую модель как можно ближе к существующей модели пользователя. Поскольку мы наследуем от AbstractBaseUser, нужно определить несколько свойств и методов:

  • USERNAME_FIELD — строка, описывающая имя поля в модели пользователя, которая используется как идентификатор. Поле должно быть уникальным (то есть иметь значение unique=True, установленное в его определении);
  • REQUIRED_FIELDS — список имен полей, которые будут запрашиваться при создании пользователя через команду управления createsuperuser;
  • is_active — логический атрибут, указывающий, является ли пользователь активным;
  • get_full_name() — более длинный формальный идентификатор для пользователя. В этом примере будем использовать полное имя пользователя, но это может быть любая строка, которая идентифицирует пользователя;
  • get_short_name() — короткий «неофициальный идентификатор» пользователя. В нашем примере — имя пользователя.

Также нужно определить UserManager. Это связано с тем, что существующий менеджер определяет методы create_user и create_superuser.

А вот как выглядит UserManager, удовлетворяющий перечисленным выше требованиям:

from django.contrib.auth.base_user import BaseUserManager

class UserManager(BaseUserManager):
    use_in_migrations = True

    def _create_user(self, email, password, **extra_fields):
        """
        Создает и сохраняет пользователя с введенным им email и паролем.
        """
        if not email:
            raise ValueError('email должен быть указан')
        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_superuser', False)
        return self._create_user(email, password, **extra_fields)

    def create_superuser(self, email, password, **extra_fields):
        extra_fields.setdefault('is_superuser', 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)

Он удаляет существующий UserManager, а также имя пользователя и свойство is_staff.

Теперь последний шаг. Нужно обновить settings.py, а именно свойство AUTH_USER_MODEL:

AUTH_USER_MODEL = 'core.User'

Таким образом мы даем понять, что нужно использовать нашу собственную модель вместо стандартной. В приведенном выше примере была создана пользовательская модель внутри приложения с именем core.

Как ссылаться на эту модель? Есть два пути. Рассмотрим модель с названием Course:

from django.db import models
from testapp.core.models import User

class Course(models.Model):
    slug = models.SlugField(max_length=100)
    name = models.CharField(max_length=100)
    tutor = models.ForeignKey(User, on_delete=models.CASCADE)

В коде все отлично, но если вы создаете многоразовое приложение, которое хотите сделать доступным для сообщества, настоятельно рекомендуется использовать следующую стратегию:

from django.db import models
from django.conf import settings

class Course(models.Model):
    slug = models.SlugField(max_length=100)
    name = models.CharField(max_length=100)
    tutor = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)

Расширение модели пользователя с помощью наследования AbstractUser

Здесь все довольно просто, поскольку класс django.contrib.auth.models.AbstractUser обеспечивает полную реализацию пользователя по умолчанию как абстрактную модель:

from django.db import models
from django.contrib.auth.models import AbstractUser

class User(AbstractUser):
    bio = models.TextField(max_length=500, blank=True)
    location = models.CharField(max_length=30, blank=True)
    birth_date = models.DateField(null=True, blank=True)

Затем вам как и в предыдущем способе нужно обновить settings.py, определяя свойство AUTH_USER_MODEL:

AUTH_USER_MODEL = 'core.User'

Это нужно сделать перед началом работ над проектом, так как это повлияет на всю схему базы данных. Также старайтесь создавать внешние ключи для модели пользователя, импортируя параметры из from django.conf import settings и ссылаясь на  settings.AUTH_USER_MODEL вместо прямого обращения к новой пользовательской модели.

Выводы

В статье мы рассмотрели четыре разных способа расширения существующей модели пользователя. Самого лучшего универсального варианта среди них нет, так что вам придется выбирать наиболее подходящий в зависимости от того, чего вы хотите добиться.

Перевод статьи «How to Extend Django User Model»

If you are a Django developer, you might have already noticed that Django comes with a built-in authentication system. Django’s authentication system provides a mechanism for user signup, login, password reset, authentication, and authorization. In this post, we’ll learn how to create a custom user model in Django.

Although we can use the default user model provided by Django’s authentication module without making any changes, there can be situations in which the default user model has to be modified.

For example, you may have to add additional fields to the user model or use email instead of a username for login.

In fact, we have a couple of different ways to customize the Django user model. We mainly use these two methods.

  1. AbstractUser
  2. AbstractBaseUser

In this post, we’ll use AbstractBaseUser to customize the user model.

The source code of this project can be downloaded from GitHub.

geekinsta/django-abstractbaseuser

Custom User model using AbstractBaseUser. Contribute to geekinsta/django-abstractbaseuser development by creating an account on GitHub.

GitHubgeekinsta

Create a Django Project

Let us start by creating a new Django project.

django-admin startproject djangouser

Navigate to the project folder using:

cd djangouser

Next, we have to create a new app called accounts. This app will hold our custom user model.

python manage.py startapp accounts

Register the app under INSTALLED_APPS in settings.py.

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'accounts'
]

Customize User Model With AbstractBaseUser

The AbstractBaseUser and AbstractUser classes provide the basic fields and features of a User model password filed and password hashing mechanism so that we don’t have to reinvent the wheel.

Open the models.py file of accounts and import AbstractBaseUser and BaseUserManager from django.contrib.auth.models.

Here’s the code of my models.py file.

from django.db import models
from django.contrib.auth.models import AbstractBaseUser, BaseUserManager
from django.contrib.auth.models import PermissionsMixin
from django.core import validators

Now, let us create our Custom User Model by creating a class named User that inherits from AbstractBaseUser.

class User(AbstractBaseUser, PermissionsMixin):

    # Primary key of the model
    id = models.BigAutoField(
        primary_key = True,
    )

    # Email field that serves as the username field
    email = models.CharField(
        max_length = 50, 
        unique = True, 
        validators = [validators.EmailValidator()],
        verbose_name = "Email"
    )

    # Other required fields for authentication
    # If the user is a staff, defaults to false
    is_staff = models.BooleanField(default=False)

    # If the user account is active or not. Defaults to True.
    # If the value is set to false, user will not be allowed to sign in.
    is_active = models.BooleanField(default=True)
    
    # Setting email instead of username
    USERNAME_FIELD = 'email'
    
    # Custom user manager
    objects = UserManager()
    
    def get_full_name(self):
        # Returns the first_name and the last_name
        return f'{self.first_name} {self.last_name}'

    def get_short_name(self):
        # Returns the short name for the user.
        return self.first_name

Note that I have not included the password field and is_superuser in this model. This is because, as I mentioned earlier, the password field will be inherited from AbstarctBaseUser class and is_superuser field will be inherited from PermissionsMixin. Also, note that we are using email instead of a username.

We can also add additional fields like First name and Last Name to the model if we want.

class User(AbstractBaseUser, PermissionsMixin):

    # Primary key of the model
    id = models.BigAutoField(
        primary_key = True,
    )

    first_name = models.CharField(
        max_length = 20,
        verbose_name = "First Name",
    )

    last_name = models.CharField(
        max_length = 20,
        verbose_name = "Last Name",
    )

    # Email field that serves as the username field
    email = models.CharField(
        max_length = 50, 
        unique = True, 
        validators = [validators.EmailValidator()],
        verbose_name = "Email"
    )

    # Other required fields for authentication
    # If the user is a staff, defaults to false
    is_staff = models.BooleanField(default=False)

    # If the user account is active or not. Defaults to True.
    # If the value is set to false, user will not be allowed to sign in.
    is_active = models.BooleanField(default=True)
    
    # Setting email instead of username
    USERNAME_FIELD = 'email'
    
    # Custom user manager
    objects = UserManager()
    
    def get_full_name(self):
        # Returns the first_name and the last_name
        return f'{self.first_name} {self.last_name}'

    def get_short_name(self):
        # Returns the short name for the user.
        return self.first_name

The default UserManager can only be used if our model contains username, email, is_staff, is_active, is_superuser, last_login, and date_joined fields. Note that we are using email instead of username and we have completely removed the username field. So, we should customize the default UserManager to make it work with our custom User Model.

In models.py, create a new UserManager above the User model.

class UserManager(BaseUserManager):
    use_in_migrations = True

    # Method to save user to the database
    def save_user(self, email, password, **extra_fields):
        """
        Creates and saves a User with the given email and password.
        """
        if not email:
            raise ValueError('The given email must be set')

        email = self.normalize_email(email)
        user = self.model(email=email, **extra_fields)

        # Call this method for password hashing
        user.set_password(password)
        user.save(using=self._db)
        return user

    def create_user(self, email, password=None, **extra_fields):
        extra_fields['is_superuser'] = False
        extra_fields['is_staff'] = False
        return self.save_user(email, password, **extra_fields)

    # Method called while creating a staff user
    def create_staffuser(self, email, password, **extra_fields):
        extra_fields['is_staff'] = True
        extra_fields['is_superuser'] = False
        
        return self.save_user(email, password, **extra_fields) 

    # Method called while calling creatsuperuser
    def create_superuser(self, email, password, **extra_fields):

        # Set is_superuser parameter to true
        extra_fields.setdefault('is_superuser', True)

        if extra_fields.get('is_superuser') is not True:
            raise ValueError('is_superuser should be True')
        
        extra_fields['is_staff'] = True

        return self.save_user(email, password, **extra_fields) 

The following line in the User model configures it to use our custom UserManager class.

objects = UserManager()

Here’s the complete code of my models.py file.

from django.db import models
from django.contrib.auth.models import AbstractBaseUser, BaseUserManager
from django.contrib.auth.models import PermissionsMixin
from django.core import validators

# Create your models here.
class UserManager(BaseUserManager):
    use_in_migrations = True

    # Method to save user to the database
    def save_user(self, email, password, **extra_fields):
        """
        Creates and saves a User with the given email and password.
        """
        if not email:
            raise ValueError('The given email must be set')

        email = self.normalize_email(email)
        user = self.model(email=email, **extra_fields)

        # Call this method for password hashing
        user.set_password(password)
        user.save(using=self._db)
        return user

    def create_user(self, email, password=None, **extra_fields):
        extra_fields['is_superuser'] = False
        extra_fields['is_staff'] = False
        return self.save_user(email, password, **extra_fields)

    # Method called while creating a staff user
    def create_staffuser(self, email, password, **extra_fields):
        extra_fields['is_staff'] = True
        extra_fields['is_superuser'] = False
        
        return self.save_user(email, password, **extra_fields) 

    # Method called while calling creatsuperuser
    def create_superuser(self, email, password, **extra_fields):

        # Set is_superuser parameter to true
        extra_fields.setdefault('is_superuser', True)

        if extra_fields.get('is_superuser') is not True:
            raise ValueError('is_superuser should be True')
        
        extra_fields['is_staff'] = True

        return self.save_user(email, password, **extra_fields) 
    

class User(AbstractBaseUser, PermissionsMixin):

    # Primary key of the model
    id = models.BigAutoField(
        primary_key = True,
    )

    first_name = models.CharField(
        max_length = 20,
        verbose_name = "First Name",
    )

    last_name = models.CharField(
        max_length = 20,
        verbose_name = "Last Name",
    )

    # Email field that serves as the username field
    email = models.CharField(
        max_length = 50, 
        unique = True, 
        validators = [validators.EmailValidator()],
        verbose_name = "Email"
    )

    # Other required fields for authentication
    # If the user is a staff, defaults to false
    is_staff = models.BooleanField(default=False)

    # If the user account is active or not. Defaults to True.
    # If the value is set to false, user will not be allowed to sign in.
    is_active = models.BooleanField(default=True)
    
    # Setting email instead of username
    USERNAME_FIELD = 'email'

    # Custom user manager
    objects = UserManager()

    def get_full_name(self):
        # Returns the first_name plus the last_name, with a space in between.
        return f'{self.first_name} {self.last_name}'

    def get_short_name(self):
        # Returns the short name for the user.
        return self.first_name

Now we have a customized Django user model and we have to tell Django to use this model instead of the default one. For that, add the following line to the end of settings.py

AUTH_USER_MODEL = 'accounts.User'

The last step is to commit the changes to database using:

python manage.py makemigrations
python manage.py migrate

If you previously made any migrations, you may get some errors at this point. The easiest way to solve this issue is to delete the existing database and migrations and run the above mentioned commands again.

You can test the User model by creating a new superuser using this command.

python manage.py createsuperuser

Instead of username, Django will now ask for email id and the login page of django admin dashboard will use email instead of username and password.

If you are facing, let me know in the comments below.

Ezoicreport this ad

В Django встроена прекрасная система аутентификации пользователей. В большинстве случаев мы можем использовать ее «из коробки», что экономит много времени разработчиков и тестировщиков. Но иногда нам необходимо расширить ее, чтобы удовлетворять потребностям нашего сайта.

Как правило возникает потребность хранить дополнительные данные о пользователях, например, краткую биографию (about), дату рождения, местоположение и другие подобные данные.

В этой статье пойдет речь о стратегиях, с помощью которых вы можете расширить пользовательскую модель Django, а не писать ее с нуля.


Стратегии расширения

Опишем кратко стратегии расширения пользовательской модели Django и потребности в их применении. А потом раскроем детали конфигурирование по каждой стратегии.

  1. Простое расширение модели (proxy)

    Эта стратегия без создания новых таблиц в базе данных. Используется, чтобы изменить поведение существующей модели (например, упорядочение по умолчанию, добавление новых методов и т.д.), не затрагивая существующую схему базы данных.

    Вы можете использовать эту стратегию, когда вам не нужно хранить дополнительную информацию в базе данных, а просто необходимо добавить дополнительные методы или изменить диспетчер запросов модели. →

  2. Использование связи один-к-одному с пользовательской моделью (user profiles)

    Это стратегия с использованием дополнительной обычный модели Django со своей таблицей в базе данных, которая связана пользователем стандартной модели через связь OneToOneField.

    Вы можете использовать эту стратегию, чтобы хранить дополнительную информацию, которая не связана с процессом аутентификации (например, дата рождения). Обычно это называется пользовательский профиль. →

  3. Расширение AbstractBaseUser

    Это стратегия использования совершенно новой модели пользователя, которая отнаследована от AbstractBaseUser. Требует особой осторожности и изменения настроек в settings.py. В идеале должно быть сделано в начале проекта, так как будет существенно влиять на схему базы данных.

    Вы можете использовать эту стратегию, когда ваш сайт имеет специфические требования в отношении процесса аутентификации. Например, в некоторых случаях имеет смысл использовать адрес электронной почты в качестве идентификации маркера вместо имени пользователя. →

  4. Расширение AbstractUser

    Это стратегия использования новой модели пользователя, которая отнаследована от AbstractUser. Требует особой осторожности и изменения настроек в settings.py. В идеале должно быть сделано в начале проекта, так как будет существенно влиять на схему базы данных.

    Вы можете использовать эту стратегию, когда сам процесс аутентификации Django вас полностью удовлетворяет и вы не хотите его менять. Тем не менее, вы хотите добавить некоторую дополнительную информацию непосредственно в модели пользователя, без необходимости создавать дополнительный класс (как в варианте 2).→


Простое расширение модели (proxy)

Это наименее трудоемкий способ расширить пользовательскую модель. Полностью ограничен в недостатках, но и не имеет никаких широких возможностей.

models.py

from django.contrib.auth.models import User
from .managers import PersonManager

class Person(User):
    objects = PersonManager()

    class Meta:
        proxy = True
        ordering = ('first_name', )

    def do_something(self):
        ...

В приведенном выше примере мы определили расширение модели User моделью Person. Мы говорим Django это прокси-модель, добавив следующее свойство внутри class Meta:

Proxy = True

Также в примере назначен пользовательский диспетчер модели, изменен порядок по умолчанию, а также определен новый метод do_something().

Стоит отметить, что User.objects.all() и Person.objects.all() будет запрашивать ту же таблицу базы данных. Единственное отличие состоит в поведении, которое мы определяем для прокси-модели.


Использование связи один-к-одному с пользовательской моделью (user profiles)

Скорее всего, это то, что вам нужно. Лично я использую этот метод в большинстве случаев. Мы будем создавать новую модель Django для хранения дополнительной информации, которая связана с моделью пользователя.

Имейте в виду, что использование этой стратегии порождает дополнительные запросы или соединения внутри запроса. В основном все время, когда вы будете запрашивать данные, будет срабатывать дополнительный запрос. Но этого можно избежать для большинства случаев. Я скажу пару слов о том, как это сделать, ниже.

models.py

from django.db import models
from django.contrib.auth.models import User

class Profile(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    bio = models.TextField(max_length=500, blank=True)
    location = models.CharField(max_length=30, blank=True)
    birth_date = models.DateField(null=True, blank=True)

Теперь добавим немножко магии: определим сигналы, чтобы наша модель Profile автоматически обновлялась при создании/изменении данных модели User.

from django.db import models
from django.contrib.auth.models import User
from django.db.models.signals import post_save
from django.dispatch import receiver

class Profile(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    bio = models.TextField(max_length=500, blank=True)
    location = models.CharField(max_length=30, blank=True)
    birth_date = models.DateField(null=True, blank=True)

@receiver(post_save, sender=User)
def create_user_profile(sender, instance, created, **kwargs):
    if created:
        Profile.objects.create(user=instance)

@receiver(post_save, sender=User)
def save_user_profile(sender, instance, **kwargs):
    instance.profile.save()

Мы «зацепили» create_user_profile() и save_user_profile() к событию сохранения модели User. Такой сигнал называется post_save.

А теперь пример шаблона Django с использованием данных Profile:

<h2>{{ user.get_full_name }}</h2>
<ul>
  <li>Username: {{ user.username }}</li>
  <li>Location: {{ user.profile.location }}</li>
  <li>Birth Date: {{ user.profile.birth_date }}</li>
</ul>

А еще можно вот так:

def update_profile(request, user_id):
    user = User.objects.get(pk=user_id)
    user.profile.bio = 'Lorem ipsum dolor sit amet, consectetur adipisicing elit...'
    user.save()

Вообще говоря, вы никогда не должны вызывать методы сохранения Profile. Все это делается с помощью модели User.

Если вам необходимо работать с формами, то ниже приведены примеры кода для этого. Помните, что вы можете за один раз (из одной формы) обрабатывать данные более одной модели (класса).

forms.py

class UserForm(forms.ModelForm):
    class Meta:
        model = User
        fields = ('first_name', 'last_name', 'email')

class ProfileForm(forms.ModelForm):
    class Meta:
        model = Profile
        fields = ('url', 'location', 'company')

views.py

@login_required
@transaction.atomic
def update_profile(request):
    if request.method == 'POST':
        user_form = UserForm(request.POST, instance=request.user)
        profile_form = ProfileForm(request.POST, instance=request.user.profile)
        if user_form.is_valid() and profile_form.is_valid():
            user_form.save()
            profile_form.save()
            messages.success(request, _('Your profile was successfully updated!'))
            return redirect('settings:profile')
        else:
            messages.error(request, _('Please correct the error below.'))
    else:
        user_form = UserForm(instance=request.user)
        profile_form = ProfileForm(instance=request.user.profile)
    return render(request, 'profiles/profile.html', {
        'user_form': user_form,
        'profile_form': profile_form
    })

profile.html

<form method="post">
  {% csrf_token %}
  {{ user_form.as_p }}
  {{ profile_form.as_p }}
  <button type="submit">Save changes</button>
</form>

И об обещанной оптимизации запросов. В полном объеме вопрос рассмотрен в другой моей статье.

Но, если коротко, то Django отношения ленивы. Django формирует запрос к таблице базы данных, если необходимо прочитать одно из ее полей. Относительно нашего примера, эффективным будет использование метода select_related().

Зная заранее, что вам необходимо получить доступ к связанным данным, вы можете c упреждением сделать это одним запросом:

users = User.objects.all().select_related('Profile')


Расширение AbstractBaseUser

Если честно, я стараюсь избегать этот метод любой ценой. Но иногда это не возможно. И это прекрасно. Едва ли существует такая вещь, как лучшее или худшее решение. По большей части, существует более или менее подходящее решение. Если это является наиболее подходящим решением для вас, что ж — идите вперед.

Я должен был сделать это один раз. Честно говоря, я не знаю, существует ли более чистый способ сделать это, но не нашел ничего другого.

Мне нужно было использовать адрес электронной почты в качестве auth token, а username абсолютно был не нужен. Кроме того, не было никакой необходимости флага is_staff, так как я не использовал Django Admin.

Вот как я определил свою собственную модель пользователя:

from __future__ import unicode_literals

from django.db import models
from django.contrib.auth.models import PermissionsMixin
from django.contrib.auth.base_user import AbstractBaseUser
from django.utils.translation import ugettext_lazy as _

from .managers import UserManager


class User(AbstractBaseUser, PermissionsMixin):
    email = models.EmailField(_('email address'), unique=True)
    first_name = models.CharField(_('first name'), max_length=30, blank=True)
    last_name = models.CharField(_('last name'), max_length=30, blank=True)
    date_joined = models.DateTimeField(_('date joined'), auto_now_add=True)
    is_active = models.BooleanField(_('active'), default=True)
    avatar = models.ImageField(upload_to='avatars/', null=True, blank=True)

    objects = UserManager()

    USERNAME_FIELD = 'email'
    REQUIRED_FIELDS = []

    class Meta:
        verbose_name = _('user')
        verbose_name_plural = _('users')

    def get_full_name(self):
        '''
        Returns the first_name plus the last_name, with a space in between.
        '''
        full_name = '%s %s' % (self.first_name, self.last_name)
        return full_name.strip()

    def get_short_name(self):
        '''
        Returns the short name for the user.
        '''
        return self.first_name

    def email_user(self, subject, message, from_email=None, **kwargs):
        '''
        Sends an email to this User.
        '''
        send_mail(subject, message, from_email, [self.email], **kwargs)

Я хотел сохранить ее как можно ближе к «стандартной» модели пользователя. Отнаследовав от AbstractBaseUser мы должны следовать некоторым правилам:

  • USERNAME_FIELD — строка с именем поля модели, которая используется в качестве уникального идентификатора (unique=True в определении);
  • REQUIRED_FIELDS — список имен полей, которые будут запрашиваться при создании пользователя с помощью команды управления createsuperuser
  • is_active — логический атрибут, который указывает, считается ли пользователь «активным»;
  • get_full_name() — длинное описание пользователя: не обязательно полное имя пользователя, это может быть любая строка, которая описывает пользователя;
  • get_short_name() — короткое описание пользователя, например, его имя или ник.

У меня был также собственный UserManager. Потому что существующий менеджер определяет create_user() и create_superuser() методы.

Мой UserManager выглядел следующим образом:

from django.contrib.auth.base_user import BaseUserManager

class UserManager(BaseUserManager):
    use_in_migrations = True

    def _create_user(self, email, password, **extra_fields):
        """
        Creates and saves a User with the given email and password.
        """
        if not email:
            raise ValueError('The given email must be set')
        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_superuser', False)
        return self._create_user(email, password, **extra_fields)

    def create_superuser(self, email, password, **extra_fields):
        extra_fields.setdefault('is_superuser', 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)

По сути, я очистил существующий UserManager от полей username и is_staff.

Последний штрих. Необходимо изменить settings.py:

AUTH_USER_MODEL = 'core.User'

Таким образом, мы говорим Django использовать нашу пользовательскую модель вместо поставляемой «в коробке». В примере выше, я создал пользовательскую модель внутри приложения с именем core.

Как ссылаться на эту модель?

Есть два способа. Рассмотрим модель под названием Course:

from django.db import models
from testapp.core.models import User

class Course(models.Model):
    slug = models.SlugField(max_length=100)
    name = models.CharField(max_length=100)
    tutor = models.ForeignKey(User, on_delete=models.CASCADE)

В целом нормально. Но, если вы планируете использовать приложение в других проектах или распространять, то рекомендуется использовать следующий подход:

from django.db import models
from django.conf import settings

class Course(models.Model):
    slug = models.SlugField(max_length=100)
    name = models.CharField(max_length=100)
    tutor = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)


Расширение AbstractUser

Это довольно просто, поскольку класс django.contrib.auth.models.AbstractUser обеспечивает полную реализацию пользовательской модели по-умолчанию в качестве абстрактной модели.

from django.db import models
from django.contrib.auth.models import AbstractUser

class User(AbstractUser):
    bio = models.TextField(max_length=500, blank=True)
    location = models.CharField(max_length=30, blank=True)
    birth_date = models.DateField(null=True, blank=True)

После этого необходимо изменить settings.py:

AUTH_USER_MODEL = 'core.User'

Как и в предыдущей стратегии, в идеале это должно быть сделано в начале проекта и с особой осторожностью, посколько изменит всю схему базы данных. Также хорошим правилом будет создавать ключи к пользовательской модели через импорт настроек from django.conf import settings и использования settings.AUTH_USER_MODEL вместо непосредственной ссылки на класс User.


Резюме

Отлично! Мы рассмотрели четыре различных стратегии расширения «стандартной» пользовательской модели. Я попытался сделать это как можно более подробно. Но, как я уже говорил, лучшего решения не существует. Все будет зависеть от того, что вы хотите получить.

  • Простое расширение модели (proxy) — вас устраивает процесс аутентификации и хранение данных пользователя, вам просто хотелось бы изменить некоторое поведение.
  • Использование связи один-к-одному с пользовательской моделью (user profiles) — вас устраивает процесс аутентификации, но хотелось бы добавить дополнительные данные пользователя (данные будут хранится в специальной модели).
  • Расширение AbstractBaseUser — процесс аутентификации «из коробки» вам не подходит.
  • Расширение AbstractUser — вас устраивает процесс аутентификации, но хотелось бы добавить дополнительные данные пользователя (данные будут хранится непосредственно в пользовательской модели, а не в специальной модели).

  • Назад
  • Обзор: Django
  • Далее

В данном руководстве мы продемонстрируем вам систему входа пользователя на ваш сайт используя его собственный аккаунт. Кроме того, мы покажем как реализовать контроль того, что может видеть и делать пользователь, в зависимости от того, залогинен он, или нет, а также имеет ли он соответствующий уровень прав доступа (permissions). Для того чтобы продемонстрировать все это, мы расширим LocalLibrary, добавив страницы для входа/выхода, а также страницы просмотра/редактирования книг, специфические для пользователя и персонала.

Требования: Завершить изучение предыдущих тем руководства, включая Руководство Django Часть 7: Работа с сессиями.
Цель: Понимать как настроить и использовать механизм аутентификации пользователя и разграничений прав доступа.

Обзор

Django предоставляет систему аутентификации и авторизации («permission») пользователя, реализованную на основе фреймворка работы с сессиями, который мы рассматривали в предыдущей части. Система аутентификации и авторизации позволяет вам проверять учётные данные пользователей и определять какие действия какой пользователь может выполнять. Данный фреймворк включает в себя встроенные модели для Пользователей и Групп (основной способ применения прав доступа для более чем одного пользователя), непосредственно саму систему прав доступа (permissions)/флаги, которые определяют может ли пользователь выполнить задачу, с какой формой и отображением для авторизованных пользователей, а так же получить доступ к контенту с ограниченным доступом.

Примечание: В соответствии с идеологией Django система аутентификации является очень общей и, таким образом, не предоставляет некоторые возможности, которые присутствуют в других системах веб-аутентификации. Решениями некоторых общих задач занимаются пакеты сторонних разработчиков, например, защита от подбора пароля (через стороннюю библиотеку OAuth).

В данном разделе руководства мы покажем вам реализацию аутентификации пользователя на сайте LocalLibrary, создание страниц входа/выхода, добавления разграничения доступа (permissions) к вашим моделям, а также продемонстрируем контроль за доступом к некоторым страницам. Мы будем использовать аутентификацию/авторизацию для показа пользователям и сотрудникам библиотеки, списков книг, которые были взяты на прокат.

Система аутентификации является очень гибкой и позволяет вам формировать свои собственные URL-адреса, формы, отображения, а также шаблоны страниц, если вы пожелаете, с нуля, через простой вызов функций соответствующего API для авторизации пользователя. Тем не менее, в данной статье мы будем использовать «встроенные» в Django методы отображений и форм аутентификации, а также методы построения страниц входа и выхода. Нам все ещё необходимо создавать шаблоны страниц, но это будет достаточно несложно.

Мы покажем вам как реализовать разграничение доступа (permissions), а также выполнять соответствующую проверку статусов авторизации и прав доступа, в отображениях, и в шаблонах страниц.

Подключение аутентификации

Аутентификация была подключена автоматически когда мы создали скелет сайта (в части 2), таким образом на данный момент вам ничего не надо делать.

Примечание: Необходимые настройки были выполнены для нас, когда мы создали приложение при помощи команды django-admin startproject. Таблицы базы данных для пользователей и модели авторизации были созданы, когда в первый раз выполнили команду python manage.py migrate.

Соответствующие настройки сделаны в параметрах INSTALLED_APPS и MIDDLEWARE файла проекта (locallibrary/locallibrary/settings.py), как показано ниже:

INSTALLED_APPS = [
    ...
    'django.contrib.auth',  # Фреймворк аутентификации и моделей по умолчанию.
    'django.contrib.contenttypes',  # Django контент-типовая система (даёт разрешения, связанные с моделями).
    ....

MIDDLEWARE = [
    ...
    'django.contrib.sessions.middleware.SessionMiddleware',  # Управление сессиями между запросами
    ...
    'django.contrib.auth.middleware.AuthenticationMiddleware',  # Связывает пользователей, использующих сессии, запросами.
    ....

Создание пользователей и групп

Вы уже создали своего первого пользователя когда мы рассматривали Административная панель сайта Django в части 4 (это был суперпользователь, созданный при помощи команды python manage.py createsuperuser). Наш суперпользователь уже авторизован и имеет все необходимые уровни доступа к данным и функциям, таким образом нам необходимо создать тестового пользователя для отработки соответствующей работы сайта. В качестве наиболее быстрого способа, мы будем использовать административную панель сайта для создания соответствующих групп и аккаунтов locallibrary.

Примечание: вы можете создавать пользователей программно, как показано ниже. Например, вам мог бы подойти данный способ в том случае, если вы разрабатываете интерфейс, который позволяет пользователям создавать их собственные аккаунты (вы не должны предоставлять доступ пользователям к административной панели вашего сайта).

from django.contrib.auth.models import User

# Создайте пользователя и сохраните его в базе данных
user = User.objects.create_user('myusername', 'myemail@crazymail.com', 'mypassword')

# Обновите поля и сохраните их снова
user.first_name = 'John'
user.last_name = 'Citizen'
user.save()

Ниже мы создадим группу, а затем пользователя. Несмотря на то, что у нас пока нет никаких разрешений для добавления к нашей библиотеке каких-либо членов, если мы захотим это сделать в будущем, то будет намного проще добавлять их к уже созданной группе, с заданной аутентификацией.

Запустите сервер разработки и перейдите к административной панели вашего сайта (http://127.0.0.1:8000/admin/). Залогиньтесь на сайте при помощи параметров (имя пользователя и пароля) аккаунта суперпользователя. Самая «верхняя» страница панели Администратора показывает все наши модели. Для того, чтобы увидеть записи в разделе Authentication and Authorisation вы можете нажать на ссылку Users, или Groups.

Admin site - add groups or users

В первую очередь, в качестве нового члена нашего сайта, давайте создадим новую группу.

  1. Нажмите на кнопку Add (Добавить) (рядом с Group) и создайте новую группу; для данной группы введите Name (Имя) «Library Members».
    Admin site - add group
  2. Для данной группы не нужны какие-либо разрешения, поэтому мы просто нажимаем кнопку SAVE (Сохранить) (вы перейдёте к списку групп).

Теперь давайте создадим пользователя:

  1. Перейдите обратно на домашнюю страницу административной панели
  2. Для перехода к диалогу добавления пользователя нажмите на кнопку Add, соответствующую строке Users (Пользователи).
    Admin site - add user pt1
  3. Введите соответствующие Username (имя пользователя) и Password/Password confirmation (пароль/подтверждение пароля) для вашего тестового пользователя
  4. Нажмите SAVE для завершения процесса создания пользователя.
    Административная часть сайта создаст нового пользователя и немедленно перенаправит вас на страницу Change user (Изменение параметров пользователя) где вы можете, соответственно, изменить ваш username, а кроме того добавить информацию для дополнительных полей модели User. Эти поля включают в себя имя пользователя, фамилию, адрес электронной почты, статус пользователя, а также соответствующие параметры доступа (может быть установлен только флаг Active). Ниже вы можете определить группу для пользователя и необходимые параметры доступа, а кроме того, вы можете увидеть важные даты, относящиеся к пользователю (дату подключения к сайту и дату последнего входа).
    Admin site - add user pt2
  5. В разделе Groups, из списка Доступные группы выберите группу Library Member, а затем переместите её в блок «Выбранные группы» (нажмите стрелку-«направо», находящуюся между блоками).
    Admin site - add user to group
  6. Больше нам не нужно здесь нечего делать, просто нажмите «Save»(Сохранить), и вы вернётесь к списку созданных пользователей.

Вот и все! Теперь у вас есть учётная запись «обычного члена библиотеки», которую вы сможете использовать для тестирования (как только добавим страницы, чтобы пользователи могли войти в систему).

Примечание: Попробуйте создать другого пользователя, например «Библиотекаря». Так же создайте группу «Библиотекарей» и добавьте туда своего только что созданного библиотекаря

Настройка представлений проверки

Django предоставляет почти все, что нужно для создания страниц аутентификации входа, выхода из системы и управления паролями из коробки. Это включает в себя url-адреса, представления (views) и формы,но не включает шаблоны — мы должны создать свой собственный шаблон!

В этом разделе мы покажем, как интегрировать систему по умолчанию в Сайт LocalLibrary и создать шаблоны. Мы поместим их в основные URL проекта.

Примечание: вы не должны использовать этот код, но вполне вероятно, что вы хотите, потому что это делает вещи намного проще. Вам почти наверняка потребуется изменить код обработки формы, если вы измените свою модель пользователя (сложная тема!) но даже в этом случае вы всё равно сможете использовать функции просмотра запасов.

**Примечание:**В этом случае мы могли бы разумно поместить страницы аутентификации, включая URL-адреса и шаблоны, в наше приложение каталога. Однако, если бы у нас было несколько приложений, было бы лучше отделить это общее поведение входа в систему и иметь его доступным на всем сайте, так что это то, что мы показали здесь!

Проектирование URLs

Добавьте следующее в нижней части проекта urls.py файл (locallibrary/locallibrary/urls.py) файл:

#Add Django site authentication urls (for login, logout, password management)
urlpatterns += [
    path('accounts/', include('django.contrib.auth.urls')),
]

Перейдите по http://127.0.0.1:8000/accounts/ URL (обратите внимание на косую черту!), Django покажет ошибку, что он не смог найти этот URL, и перечислить все URL, которые он пытался открыть. Из этого вы можете увидеть URL-адреса, которые будут работать, например:

Примечание: Примечание. Использование вышеуказанного метода добавляет следующие URL-адреса с именами в квадратных скобках, которые могут использоваться для изменения сопоставлений URL-адресов. Вам не нужно реализовывать что-либо ещё — приведённое выше сопоставление URL-адресов автоматически отображает указанные ниже URL-адреса.

Примечание:

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']

Теперь попробуйте перейти к URL-адресу входа (http://127.0.0.1:8000/accounts/login/). Это приведёт к сбою снова, но с ошибкой, сообщающей вам, что нам не хватает требуемого шаблона (registration / login.html) в пути поиска шаблона. Вы увидите следующие строки, перечисленные в жёлтом разделе вверху:

Exception Type:    TemplateDoesNotExist
Exception Value:    registration/login.html

Следующий шаг — создать каталог регистрации в пути поиска, а затем добавить файл login.html.

Каталог шаблонов

URL-адреса (и неявные представления), которые мы только что добавили, ожидают найти связанные с ними шаблоны в каталоге / регистрации / где-то в пути поиска шаблонов.

Для этого сайта мы разместим наши HTML-страницы в каталоге templates / registration /. Этот каталог должен находиться в корневом каталоге проекта, то есть в том же каталоге, что и в каталоге и папках locallibrary). Создайте эти папки сейчас.

Примечание: ваша структура папок теперь должна выглядеть как показано внизу:
locallibrary (django project folder)
|_catalog
|_locallibrary
|_templates (new)
|_registration

Чтобы сделать эти директории видимыми для загрузчика шаблонов (т. е. помещать этот каталог в путь поиска шаблона) откройте настройки проекта (/locallibrary/locallibrary/settings.py), и обновите в секции TEMPLATES строку 'DIRS' как показано.

TEMPLATES = [
    {
        ...
        'DIRS': [os.path.join(BASE_DIR, 'templates')],
        'APP_DIRS': True,
        ...

Шаблон аутентификации

Предупреждение: Важно: Шаблоны аутентификации, представленные в этой статье, являются очень простой / слегка изменённой версией шаблонов логина демонстрации Django. Возможно, вам придётся настроить их для собственного использования!

Создайте новый HTML файл, названный /locallibrary/templates/registration/login.html. дайте ему следующее содержание:

{% extends "base_generic.html" %}

{% block content %}

{% if form.errors %}
  <p>Your username and password didn't match. Please try again.</p>
{% endif %}

{% if next %}
  {% if user.is_authenticated %}
    <p>Your account doesn't have access to this page. To proceed,
    please login with an account that has access.</p>
  {% else %}
    <p>Please login to see this page.</p>
  {% endif %}
{% endif %}

<form method="post" action="{% url 'login' %}">
{% csrf_token %}
<table>

<tr>
  <td>{{ form.username.label_tag }}</td>
  <td>{{ form.username }}</td>
</tr>

<tr>
  <td>{{ form.password.label_tag }}</td>
  <td>{{ form.password }}</td>
</tr>
</table>

<input type="submit" value="login" />
<input type="hidden" name="next" value="{{ next }}" />
</form>

{# Assumes you setup the password_reset view in your URLconf #}
<p><a href="{% url 'password_reset' %}">Lost password?</a></p>

{% endblock %}

Этот шаблон имеет сходство с тем, что мы видели раньше — он расширяет наш базовый шаблон и переопределяет блок контента. Остальная часть кода — это довольно стандартный код обработки формы, о котором мы поговорим в следующем учебном пособии. Все, что вам нужно знать, это показ формы, в которой вы можете ввести своё имя пользователя и пароль, а если вы введёте недопустимые значения, вам будет предложено ввести правильные значения, когда страница обновится.

Перейдите на страницу входа (http://127.0.0.1:8000/accounts/login/) когда вы сохраните свой шаблон, и вы должны увидеть что-то наподобие этого:

Library login page v1

Если ваша попытка войти в систему будет успешной, вы будете перенаправлены на другую страницу (по умолчанию это будет http://127.0.0.1:8000/accounts/profile/). Проблема здесь в том, что по умолчанию Django ожидает, что после входа в систему вы захотите перейти на страницу профиля, что может быть или не быть. Поскольку вы ещё не определили эту страницу, вы получите ещё одну ошибку!

Откройте настройки проекта (/locallibrary/locallibrary/settings.py) и добавьте текст ниже. Теперь, когда вы входите в систему, вы по умолчанию должны перенаправляться на домашнюю страницу сайта.

# Redirect to home URL after login (Default redirects to /accounts/profile/)
LOGIN_REDIRECT_URL = '/'

Шаблон выхода

Если вы перейдёте по URL-адресу выхода (http://127.0.0.1:8000/accounts/logout/), то увидите странное поведение — ваш пользователь наверняка выйдет из системы, но вы попадёте на страницу выхода администратора. Это не то, что вам нужно, хотя бы потому, что ссылка для входа на этой странице приведёт вас к экрану входа в систему администратора. (и это доступно только для пользователей, у которых есть разрешение is_staff).

Создайте и откройте /locallibrary/templates/registration/logged_out.html. Скопируйте текст ниже:

{% extends "base_generic.html" %}

{% block content %}
<p>Logged out!</p>

<a href="{% url 'login'%}">Click here to login again.</a>
{% endblock %}

Этот шаблон очень прост. Он просто отображает сообщение, информирующее вас о том, что вы вышли из системы, и предоставляет ссылку, которую вы можете нажать, чтобы вернуться на экран входа в систему. Если вы снова перейдёте на страницу выхода из системы, вы увидите эту страницу:

Library logout page v1

Шаблон сброса пароля

Система сброса пароля по умолчанию использует электронную почту, чтобы отправить пользователю ссылку на сброс. Вам необходимо создать формы, чтобы получить адрес электронной почты пользователя, отправить электронное письмо, разрешить им вводить новый пароль и отметить, когда весь процесс будет завершён.

В качестве отправной точки можно использовать следующие шаблоны.

Форма сброса пароля

Это форма, используемая для получения адреса электронной почты пользователя (для отправки пароля для сброса пароля). Создайте /locallibrary/templates/registration/password_reset_form.html и дайте ему следующее содержание:

{% extends "base_generic.html" %}
{% block content %}

<form action="" method="post">{% csrf_token %}
    {% if form.email.errors %} {{ form.email.errors }} {% endif %}
        <p>{{ form.email }}</p>
    <input type="submit" class="btn btn-default btn-lg" value="Reset password" />
</form>

{% endblock %}

Сброс пароля

Эта форма отображается после того, как ваш адрес электронной почты будет собран. Создайте /locallibrary/templates/registration/password_reset_done.html, и дайте ему следующее содержание:

{% extends "base_generic.html" %}
{% block content %}
<p>We've emailed you instructions for setting your password. If they haven't arrived in a few minutes, check your spam folder.</p>
{% endblock %}

Сброс пароля по email

Этот шаблон предоставляет текст электронной почты HTML, содержащий ссылку на сброс, которую мы отправим пользователям. Создайте /locallibrary/templates/registration/password_reset_email.html и дайте ему следующее содержание:

Someone asked for password reset for email {{ email }}. Follow the link below:
{{ protocol}}://{{ domain }}{% url 'password_reset_confirm' uidb64=uid token=token %}

Подтверждение на сброс пароля

На этой странице вы вводите новый пароль после нажатия ссылки в электронном письме с возвратом пароля. Создайте /locallibrary/templates/registration/password_reset_confirm.html и дайте ему следующее содержание:

{% extends "base_generic.html" %}

{% block content %}

    {% if validlink %}
        <p>Please enter (and confirm) your new password.</p>
        <form action="" method="post">
            {% csrf_token %}
            <table>
                <tr>
                    <td>{{ form.new_password1.errors }}
                        <label for="id_new_password1">New password:</label></td>
                    <td>{{ form.new_password1 }}</td>
                </tr>
                <tr>
                    <td>{{ form.new_password2.errors }}
                        <label for="id_new_password2">Confirm password:</label></td>
                    <td>{{ form.new_password2 }}</td>
                </tr>
                <tr>
                    <td></td>
                    <td><input type="submit" value="Change my password" /></td>
                </tr>
            </table>
        </form>
    {% else %}
        <h1>Password reset failed</h1>
        <p>The password reset link was invalid, possibly because it has already been used. Please request a new password reset.</p>
    {% endif %}

{% endblock %}

Сброс пароля завершён

Это последний шаблон сброса пароля, который отображается, чтобы уведомить вас о завершении сброса пароля. Создайте /locallibrary/templates/registration/password_reset_complete.html и дайте ему следующее содержание:

{% extends "base_generic.html" %}
{% block content %}

<h1>The password has been changed!</h1>
<p><a href="{% url 'login' %}">log in again?</a></p>

{% endblock %}

Тестирование новых страниц аутентификации

Теперь, когда вы добавили конфигурацию URL и создали все эти шаблоны, теперь страницы аутентификации должны работать! Вы можете протестировать новые страницы аутентификации, попытавшись войти в систему, а затем выйдите из учётной записи суперпользователя, используя эти URL-адреса:

  • http://127.0.0.1:8000/accounts/login/
  • http://127.0.0.1:8000/accounts/logout/

Вы сможете проверить функцию сброса пароля по ссылке на странице входа. Имейте в виду, что Django отправляет только сбросные электронные письма на адреса (пользователи), которые уже хранятся в его базе данных!

Примечание: Система сброса пароля требует, чтобы ваш сайт поддерживал электронную почту, что выходит за рамки этой статьи, поэтому эта часть ещё не будет работать. Чтобы разрешить тестирование, поместите следующую строку в конец файла settings.py. Это регистрирует любые письма, отправленные на консоль (чтобы вы могли скопировать ссылку на сброс пароля с консоли).

EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'

Для получения дополнительной информации см. Отправка email (Django docs).

Тестирование проверки подлинности пользователей

В этом разделе мы рассмотрим, что мы можем сделать, чтобы выборочно контролировать контент, который видят пользователи, на основе того, вошли ли они в систему или нет.

Тестирование в шаблонах

Вы можете получить информацию о текущем зарегистрированном пользователе в шаблонах с переменной шаблона {{user}} (это добавляется в контекст шаблона по умолчанию при настройке проекта, как и в нашем скелете).

Обычно вы сначала проверяете переменную шаблона {{user.is_authenticated}}, чтобы определить, имеет ли пользователь право видеть конкретный контент. Чтобы продемонстрировать это, мы обновим нашу боковую панель, чтобы отобразить ссылку «Вход», если пользователь вышел из системы, и ссылку «Выход», если он вошёл в систему.

Откройте базовый шаблон (/locallibrary/catalog/templates/base_generic.html) и скопируйте следующий текст в sidebar блок непосредственно перед тегом шаблона endblock.

  <ul class="sidebar-nav">

    ...

   {% if user.is_authenticated %}
     <li>User: {{ user.get_username }}</li>
     <li><a href="{% url 'logout'%}?next={{request.path}}">Logout</a></li>
   {% else %}
     <li><a href="{% url 'login'%}?next={{request.path}}">Login</a></li>
   {% endif %}
  </ul>

Как вы можете видеть, мы используем теги шаблона if-else-endif для условного отображения текста на основе того, является ли {{user.is_authenticated}} истинным. Если пользователь аутентифицирован, мы знаем, что у нас есть действительный пользователь, поэтому мы вызываем {{user.get_username}}, чтобы отобразить их имя.

Мы создаём URL-адрес для входа и выхода из системы, используя тег шаблона URL-адреса и имена соответствующих конфигураций URLs. Также обратите внимание на то, как мы добавили ?next={{request.path}} в конец URLs. Это означает, что следующий URL-адрес содержит адрес (URL) текущей страницы, в конце связанного URL-адреса. После того, как пользователь успешно выполнил вход в систему, представления будут использовать значение «next» чтобы перенаправить пользователя обратно на страницу, где они сначала нажали ссылку входа / выхода из системы.

Примечание: Попробуйте! Если вы находитесь на главной странице и вы нажимаете «Вход / Выход» на боковой панели, то после завершения операции вы должны вернуться на ту же страницу.

Тестирование в представлениях

Если вы используете функциональные представления, самым простым способом ограничить доступ к вашим функциям является применение login_required декоратор к вашей функции просмотра, как показано ниже. Если пользователь вошёл в систему, ваш код просмотра будет выполняться как обычно. Если пользователь не вошёл в систему, это перенаправит URL-адрес входа, определённый в настройках проекта. (settings.LOGIN_URL), передав текущий абсолютный путь в качестве next параметра URL. Если пользователю удастся войти в систему, они будут возвращены на эту страницу, но на этот раз аутентифицированы.

from django.contrib.auth.decorators import login_required

@login_required
def my_view(request):
    ...

Примечание: Вы можете сделать то же самое вручную, путём тестирования request.user.is_authenticated, но декоратор намного удобнее!

Аналогичным образом, самый простой способ ограничить доступ к зарегистрированным пользователям в ваших представлениях на основе классов — это производные от LoginRequiredMixin. Вы должны объявить этот mixin сначала в списке суперкласса, перед классом основного представления.

from django.contrib.auth.mixins import LoginRequiredMixin

class MyView(LoginRequiredMixin, View):
    ...

Это имеет такое же поведение при переадресации, что и login_required декоратор. Вы также можете указать альтернативное местоположение для перенаправления пользователя, если он не аутентифицирован (login_url), и имя параметра URL вместо «next» , чтобы вставить текущий абсолютный путь (redirect_field_name).

class MyView(LoginRequiredMixin, View):
    login_url = '/login/'
    redirect_field_name = 'redirect_to'

Для получения дополнительной информации ознакомьтесь с Django docs here.

Пример — перечисление книг текущего пользователя

Теперь, когда мы знаем, как ограничить страницу определённому пользователю, создайте представление о книгах, которые заимствовал текущий пользователь.

К сожалению, у нас пока нет возможности пользователям использовать книги! Поэтому, прежде чем мы сможем создать список книг, мы сначала расширим BookInstance модель для поддержки концепции заимствования и использования приложения Django Admin для заимствования ряда книг нашему тестовому пользователю.

Модели

Прежде всего, мы должны предоставить пользователям возможность кредита на BookInstance (у нас уже есть status и due_back дата, но у нас пока нет связи между этой моделью и пользователем. Мы создадим его с помощью поля ForeignKey (один ко многим). Нам также нужен простой механизм для проверки того, просрочена ли заёмная книга.

Откройте catalog/models.py, и импортируйте модель User из django.contrib.auth.models (добавьте это чуть ниже предыдущей строки импорта в верхней части файла, так User доступен для последующего кода, что позволяет использовать его):

from django.contrib.auth.models import User

Затем добавьте поле borrower в модель BookInstance:

borrower = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True)

Пока мы здесь, давайте добавим свойство, которое мы можем вызвать из наших шаблонов, чтобы указать, просрочен ли конкретный экземпляр книги. Хотя мы могли бы рассчитать это в самом шаблоне, использование свойства, как показано ниже, будет намного более эффективным. Добавьте это где-нибудь в верхней части файла:

from datetime import date

Теперь добавьте следующее определение свойства внутри класса BookInstance:

@property
def is_overdue(self):
    if self.due_back and date.today() > self.due_back:
        return True
    return False

Примечание: Примечание. Сначала мы проверим, является ли due_back пустым, прежде чем проводить сравнение. Пустое поле due_back заставило Django выкидывать ошибку, а не показывать страницу: пустые значения не сопоставимы. Это не то, что мы хотели бы, чтобы наши пользователи испытывали!

Теперь, когда мы обновили наши модели, нам нужно будет внести новые изменения в проект, а затем применить эти миграции:

python3 manage.py makemigrations
python3 manage.py migrate

Admin

Теперь откройте каталог catalog/admin.py, и добавьте поле borrower в класс BookInstanceAdmin , как в list_display , так и в полях fieldsets , как показано ниже. Это сделает поле видимым в разделе Admin, так что мы можем при необходимости назначить User в BookInstance.

@admin.register(BookInstance)
class BookInstanceAdmin(admin.ModelAdmin):
    list_display = ('book', 'status', 'borrower', 'due_back', 'id')
    list_filter = ('status', 'due_back')

    fieldsets = (
        (None, {
            'fields': ('book','imprint', 'id')
        }),
        ('Availability', {
            'fields': ('status', 'due_back','borrower')
        }),
    )

Займите несколько книг

Теперь, когда возможно кредитовать книги конкретному пользователю, зайдите и заработайте на нескольких записей в BookInstance. Установите borrowed поле вашему тестовому пользователю, сделайте status «В займе» и установите сроки оплаты как в будущем, так и в прошлом.

Примечание: Мы не будем описывать процесс, так как вы уже знаете, как использовать Admin сайт!

Займ в представлении

Теперь мы добавим представление для получения списка всех книг, которые были предоставлены текущему пользователю. Мы будем использовать один и тот же общий класс, с которым мы знакомы, но на этот раз мы также будем импортировать и выводить из LoginRequiredMixin, так что только вошедший пользователь сможет вызвать это представление. Мы также решили объявить template_name, вместо того, чтобы использовать значение по умолчанию, потому что у нас может быть несколько разных списков записей BookInstance, с разными представлениями и шаблонами.

Добавьте следующее в catalog/views.py:

from django.contrib.auth.mixins import LoginRequiredMixin

class LoanedBooksByUserListView(LoginRequiredMixin,generic.ListView):
    """
    Generic class-based view listing books on loan to current user.
    """
    model = BookInstance
    template_name ='catalog/bookinstance_list_borrowed_user.html'
    paginate_by = 10

    def get_queryset(self):
        return BookInstance.objects.filter(borrower=self.request.user).filter(status__exact='o').order_by('due_back')

Чтобы ограничить наш запрос только объектами BookInstance для текущего пользователя, мы повторно реализуем get_queryset(), как показано выше. Обратите внимание, что «o» это сохранённый код для «on loan» и мы сортируем по дате due_back, чтобы сначала отображались самые старые элементы.

URL-адрес для заёмных книг

Теперь откройте /catalog/urls.py и добавьте url() , указывая на приведённое выше представление (вы можете просто скопировать текст ниже в конец файла).

urlpatterns += [
    url(r'^mybooks/$', views.LoanedBooksByUserListView.as_view(), name='my-borrowed'),
]

Шаблон для заёмных книг

Теперь все, что нам нужно сделать для этой страницы, — это добавить шаблон. Сначала создайте файл шаблона /catalog/templates/catalog/bookinstance_list_borrowed_user.html и дайте ему следующее содержание:

{% extends "base_generic.html" %}

{% block content %}
    <h1>Borrowed books</h1>

    {% if bookinstance_list %}
    <ul>

      {% for bookinst in bookinstance_list %}
      <li class="{% if bookinst.is_overdue %}text-danger{% endif %}">
        <a href="{% url 'book-detail' bookinst.book.pk %}">{{bookinst.book.title}}</a> ({{ bookinst.due_back }})
      </li>
      {% endfor %}
    </ul>

    {% else %}
      <p>There are no books borrowed.</p>
    {% endif %}
{% endblock %}

Этот шаблон очень похож на тот, который мы создали ранее для объектов Book и Author. Единственное, что «новое» здесь, это то, что мы проверяем метод, который мы добавили в модель (bookinst.is_overdue) с целью использовать его для изменения цвета просроченных предметов.

Когда сервер разработки запущен, вы должны теперь иметь возможность просматривать список для зарегистрированного пользователя в своём браузере по адресу http://127.0.0.1:8000/catalog/mybooks/. Попробуйте это, когда ваш пользователь войдёт в систему и выйдет из системы (во втором случае вы должны быть перенаправлены на страницу входа в систему).

Добавить список на боковую панель

Последний шаг — добавить ссылку на эту новую страницу в sidebar. Мы поместим это в тот же раздел, где мы покажем другую информацию для зарегистрированного пользователя.

Откройте базовый шаблон (/locallibrary/catalog/templates/base_generic.html) и добавьте выделенную строку из sidebar, как показано на рисунке.

 <ul class="sidebar-nav">
   {% if user.is_authenticated %}
   <li>User: {{ user.get_username }}</li>
   <li><a href="{% url 'my-borrowed' %}">My Borrowed</a></li>
   <li><a href="{% url 'logout'%}?next={{request.path}}">Logout</a></li>
   {% else %}
   <li><a href="{% url 'login'%}?next={{request.path}}">Login</a></li>
   {% endif %}
 </ul>

На что это похоже?

Когда любой пользователь войдёт в систему, он будет видеть ссылку «Мной позаимствовано (My Borrowed)» в боковой колонке, и список книг, показанных ниже (первая книга не имеет установленной даты, что является ошибкой, которую мы надеемся исправить в более позднем уроке!).

Library - borrowed books by user

Права доступа

Права доступа связаны с моделями и определяют операции, которые могут выполняться на экземпляре модели самим пользователем, у которого есть разрешение. По умолчанию Django автоматически даёт добавить, изменить, и удалить разрешения у всех моделей, которые позволяют пользователям с правом доступа выполнять связанные действия через администратора сайта. Вы можете определить свои собственные разрешения для моделей и предоставить их конкретным пользователям. Вы также можете изменить разрешения, связанные с разными экземплярами одной и той же модели. Тестирование разрешений в представлениях и шаблонах очень похоже на тестирование по статусу аутентификации (фактически, тестирование прав доступа также проверяет аутентификацию).

Модели

Определение разрешений выполняется в разделе моделей «class Meta» , используется permissions поле. Вы можете указать столько разрешений, сколько необходимо в кортеже, причём каждое разрешение определяется во вложенном кортеже, содержащем имя разрешения и отображаемое значение разрешения. Например, мы можем определить разрешение, позволяющее пользователю отметить, что книга была возвращена, как показано здесь:

class BookInstance(models.Model):
    ...
    class Meta:
        ...
        permissions = (("can_mark_returned", "Set book as returned"),)

Затем мы могли бы назначить разрешение группе «Библиотекарь» (Librarian) на сайте администратора.

Откройте catalog/models.py, и добавьте разрешение, как показано выше. Вам нужно будет повторно выполнить миграцию (вызвав python3 manage.py makemigrations и python3 manage.py migrate) для надлежащего обновления базы данных.

Шаблоны

Разрешения текущего пользователя хранятся в переменной шаблона, называемой {{ perms }}. Вы можете проверить, имеет ли текущий пользователь определённое разрешение, используя конкретное имя переменной в соответствующем приложении «Django» — например, {{ perms.catalog.can_mark_returned }} будет True если у пользователя есть это разрешение, а False — в противном случае. Обычно мы проверяем разрешение с использованием шаблона {% if %}, как показано в:

{% if perms.catalog.can_mark_returned %}
    <!-- We can mark a BookInstance as returned. -->
    <!-- Perhaps add code to link to a "book return" view here. -->
{% endif %}

Представления

Разрешения можно проверить в представлении функции, используя permission_required декоратор или в представлении на основе классов, используя PermissionRequiredMixin. шаблон и поведение такие же, как для аутентификации входа в систему, хотя, конечно, вы можете разумно добавить несколько разрешений.

Функция в представлении с декоратором:

from django.contrib.auth.decorators import permission_required

@permission_required('catalog.can_mark_returned')
@permission_required('catalog.can_edit')
def my_view(request):
    ...

Требуется разрешение mixin для представлений на основе классов.

from django.contrib.auth.mixins import PermissionRequiredMixin

class MyView(PermissionRequiredMixin, View):
    permission_required = 'catalog.can_mark_returned'
    # Or multiple permissions
    permission_required = ('catalog.can_mark_returned', 'catalog.can_edit')
    # Note that 'catalog.can_edit' is just an example
    # the catalog application doesn't have such permission!

Пример

Мы не будем обновлять LocalLibrary здесь; возможно, в следующем уроке!

Испытайте себя

Ранее в этой статье мы показали вам, как создать страницу для текущего пользователя, в которой перечислены книги, которые они заимствовали. Теперь задача состоит в том, чтобы создать аналогичную страницу, которая видна только для библиотекарей, которая отображает все книги, которые были заимствованы, и которая показывает имя каждого заёмщика.

Вы должны следовать той же схеме, что и для другого представления. Главное отличие состоит в том, что вам нужно ограничить представление только библиотекарями. Вы можете сделать это на основе того, является ли пользователь сотрудником (декоратор функции: staff_member_required, переменная шаблона: user.is_staff) но мы рекомендуем вам вместо этого использовать can_mark_returned разрешения и PermissionRequiredMixin, как описано в предыдущем разделе.

Предупреждение: Важно: Не забудьте использовать вашего суперпользователя для тестирования на основе разрешений (проверки разрешений всегда возвращают true для суперпользователей, даже если разрешение ещё не определено!). Вместо этого создайте пользователя-библиотекаря и добавьте необходимые возможности.

Когда вы закончите, ваша страница должна выглядеть примерно, как на скриншоте ниже.

All borrowed books, restricted to librarian

Подводим итоги

Отличная работа — теперь вы создали веб-сайт, на котором участники библиотеки могут входить в систему и просматривать собственный контент, и библиотекари (с правом доступа) могут просматривать все заёмные книги с их читателями. На данный момент мы все ещё просто просматриваем контент, но те же принципы и методы используются, когда вы хотите начать изменять и добавлять данные.

В следующей статье мы рассмотрим, как вы можете использовать формы Django для сбора пользовательского ввода, а затем начнём изменять некоторые из наших сохранённых данных.

Смотрите также

Понравилась статья? Поделить с друзьями:

Читайте также:

  • Как изменить данные получателя посылки почта россии
  • Как изменить гудок на телефоне мегафон
  • Как изменить данные на мос ру снилс
  • Как изменить данные паспорта на сайте ржд
  • Как изменить данные на мос ру при смене фамилии

  • 0 0 голоса
    Рейтинг статьи
    Подписаться
    Уведомить о
    guest

    0 комментариев
    Старые
    Новые Популярные
    Межтекстовые Отзывы
    Посмотреть все комментарии