Главная страница
Навигация по странице:

  • Рис. 19.1.

  • Рис. 19.5.

  • Книга Изучаем Python


    Скачать 4.68 Mb.
    НазваниеКнига Изучаем Python
    Дата10.12.2022
    Размер4.68 Mb.
    Формат файлаpdf
    Имя файлаErik_Metiz_Izuchaem_Python_Programmirovanie_igr_vizualizatsia_da.pdf
    ТипКнига
    #837531
    страница46 из 53
    1   ...   42   43   44   45   46   47   48   49   ...   53
    412 Глава 19 • Учетные записи пользователей мой; в данном случае данные возвращаются функции представления new_topic()
    Аргумент method приказывает браузеру отправить данные в запросе типа POST.
    Django использует шаблонный тег
    {%
    csrf_token
    %}
     для предотвращения по- пыток получения несанкционированного доступа к серверу (атаки такого рода называются межсайтовой подделкой запросов). В точке  отображается форма; это наглядный пример того, насколько легко в Django выполняются такие стан- дартные операции, как отображение формы. Чтобы автоматически создать все поля, необходимые для отображения формы, достаточно включить шаблонную переменную
    {{
    form.as_p
    }}
    . Модификатор as_p приказывает Django отобра- зить все элементы формы в формате абзацев — это простой способ аккуратного отображения формы.
    Django не создает кнопку отправки данных для форм, поэтому мы определяем ее в точке .
    Создание ссылки на страницу new_topic
    Далее ссылка на страницу new_topic создается на странице topics
    :
    topics.html
    {% extends "learning_logs/base.html" %}
    {% block content %}
    Topics


    Add a new topic:
    {% endblock content %}
    Разместите ссылку после списка существующих тем. Полученная форма изобра- жена на рис. 19.1. Воспользуйтесь ею и добавьте несколько своих тем.
    Рис. 19.1. Страница для добавления новой темы

    Редактирование данных 413
    Добавление новых записей
    Теперь, когда пользователь может добавлять новые темы, он также захочет добав- лять новые записи. Мы снова определим URL, напишем новую функцию и шаблон и создадим ссылку на страницу. Но сначала нужно добавить в forms .py еще один класс.
    Класс EntryForm
    Мы должны создать форму, связанную с моделью
    Entry
    , но более специализиро- ванную по сравнению с
    TopicForm
    :
    forms.py
    from django import forms from .models import Topic, Entry class TopicForm(forms.ModelForm):
    class EntryForm(forms.ModelForm):
    class Meta:
    model = Entry fields = ['text']
     labels = {'text': ''}
     widgets = {'text': forms.Textarea(attrs={'cols': 80})}
    Сначала в команду import к
    Topic добавляется
    Entry
    . Новый класс
    EntryForm на- следует от forms.ModelForm и содержит вложенный класс
    Meta с указанием модели, на которой он базируется, и поле, включаемое в форму. Полю 'text'
    снова назна- чается пустая надпись .
    В точке  включается атрибут widgets
    . Виджет (widget) представляет собой эле- мент формы HTML: однострочное или многострочное текстовое поле, раскрываю- щийся список и т. д. Включая атрибут widgets
    , вы можете переопределить виджеты, выбранные Django по умолчанию. Приказывая Django использовать элемент forms.
    Textarea
    , мы настраиваем виджет ввода для поля 'text'
    , чтобы ширина текстовой области составляла 80 столбцов вместо значения по умолчанию 40. У пользователя будет достаточно места для создания содержательных записей.
    URL-адрес для new_entry
    Необходимо включить аргумент topic_id в URL-адрес для создания новой записи, потому что запись должна ассоциироваться с конкретной темой. Вот как выглядит
    URL, который мы добавляем в learning_logs/urls .py
    :
    urls.py
    urlpatterns = [
    # Страница для добавления новой записи url(r'^new_entry/(?P\d+)/$', views.new_entry, name='new_entry'),
    ]

    414 Глава 19 • Учетные записи пользователей
    Эта схема URL соответствует любому URL-адресу в форме http://
    localhost:8000/new_entry/id/, где id — число, равное идентификатору темы.
    Выражение
    (?P\d+)
    захватывает числовое значение и сохраняет его в переменной topic_id
    . При запросе URL-адреса, соответствующего этой схеме, Django передает запрос и идентификатор темы функции представления new_entry()
    Функция представления new_entry()
    Функция представления new_entry очень похожа на функцию добавления новой темы:
    views.py
    from django.shortcuts import render from .models import Topic from .forms import TopicForm, EntryForm def new_entry(request, topic_id):
    """Добавляет новую запись по конкретной теме."""
     topic = Topic.objects.get(id=topic_id)
     if request.method != 'POST':
    # Данные не отправлялись; создается пустая форма.
     form = EntryForm() else:
    # Отправлены данные POST; обработать данные.
     form = EntryForm(data=request.POST)
    if form.is_valid():
     new_entry = form.save(commit=False)
     new_entry.topic = topic new_entry.save()
     return HttpResponseRedirect(reverse('learning_logs:topic',
    args=[topic_id]))
    context = {'topic': topic, 'form': form}
    return render(request, 'learning_logs/new_entry.html', context)
    Мы обновляем команду import и включаем в нее только что созданный класс
    EntryForm
    . Определение new_entry()
    содержит параметр topic_id для сохранения полученного значения из URL. Идентификатор темы понадобится для отображе- ния страницы и обработки данных формы, поэтому мы используем topic_id для получения правильного объекта темы .
    В точке  проверяется метод запроса: POST или GET. Блок if выполняется для запроса GET, и мы создаем пустой экземпляр
    EntryForm
    . Для метода запроса
    POST мы обрабатываем данные, создавая экземпляр
    EntryForm
    , заполненный данными POST из объекта запроса . Затем проверяется корректность данных формы. Если данные корректны, необходимо задать атрибут topic объекта записи перед сохранением его в базе данных.

    Редактирование данных 415
    При вызове save()
    мы включаем аргумент commit=False
     для того, чтобы создать новый объект записи и сохранить его в new_entry
    , не сохраняя пока в базе данных.
    Мы присваиваем атрибуту topic объекта new_entry тему, прочитанную из базы дан- ных в начале функции

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

    пользователь перенаправляется на страницу темы. При вызове reverse()
    должны передаваться два аргумента: имя схемы URL, для которой генерируется URL-адрес, и список аргументов со всеми аргументами, которые должны быть включены в URL. Список аргументов содержит всего один элемент topic_id
    . Вызов
    HttpResponseRedirect()
    перенаправляет пользователя на стра- ницу темы, для которой была создана запись, и пользователь видит новую запись в списке записей.
    Шаблон new_entry
    Как видно из следующего кода, шаблон new_entry похож на шаблон new_topic
    :
    new_entry.html
    {% extends "learning_logs/base.html" %}
    {% block content %}

    {{ topic }}
    Add a new entry:


    {% csrf_token %}
    {{ form.as_p }}


    {% endblock content %}
    В начале страницы выводится тема , чтобы пользователь мог видеть, в какую тему добавляется новая запись. Тема также служат ссылкой для возврата к основной странице этой темы.
    Аргумент action формы включает значение topic_id из URL, чтобы функция представления могла связать новую запись с правильной темой . В остальном этот шаблон почти не отличается от new_topic .html
    Создание ссылки на страницу new_entry
    Затем необходимо создать ссылку на страницу new_entry на каждой странице темы:
    topic.html
    {% extends "learning_logs/base.html" %}
    {% block content %}
    Topic: {{ topic }}

    416 Глава 19 • Учетные записи пользователей
    Entries:
    add new entry


    {% endblock content %}
    Ссылка добавляется перед выводом записей, потому что добавление новой запи- си является самым частым действием на этой странице. На рис. 19.2 изображена страница new_entry
    . Теперь пользователь может добавить сколько угодно новых тем и новых записей по каждой теме. Опробуйте страницу new_entry
    , добавив не- сколько записей для каждой из созданных вами тем.
    Рис. 19.2. Страница new_entry
    Добавление записей
    А теперь мы создадим страницу, на которой пользователи смогут редактировать ранее добавленные записи.
    URL-адрес для edit_entry
    В URL-адресе страницы должен передаваться идентификатор редактируемой записи. В файл learning_logs/urls .py для этого вносятся следующие изменения:
    urls.py
    urlpatterns = [
    # Страница для редактирования записи url(r'^edit_entry/(?P\d+)/$', views.edit_entry,
    name='edit_entry'),
    ]

    Редактирование данных 417
    Идентификатор, переданный в URL (например, http://localhost:8000/edit_entry/1/), сохраняется в параметре entry_id
    . Схема The URL отправляет запросы, соответ- ствующие этому формату, функции представления edit_entry()
    Функция представления edit_entry()
    Когда страница edit_entry получает запрос GET, edit_entry()
    возвращает форму для редактирования записи. При получении запроса POST с отредактированной записью страница сохраняет измененный текст в базе данных:
    views.py
    from django.shortcuts import render from .models import Topic, Entry from .forms import TopicForm, EntryForm def edit_entry(request, entry_id):
    """Редактирует существующую запись."""
     entry = Entry.objects.get(id=entry_id)
    topic = entry.topic if request.method != 'POST':
    # Исходный запрос; форма заполняется данными текущей записи.
     form = EntryForm(instance=entry)
    else:
    # Отправка данных POST; обработать данные.
     form = EntryForm(instance=entry, data=request.POST)
    if form.is_valid():
     form.save()
     return HttpResponseRedirect(reverse('learning_logs:topic',
    args=[topic.id]))
    context = {'entry': entry, 'topic': topic, 'form': form}
    return render(request, 'learning_logs/edit_entry.html', context)
    Сначала необходимо импортировать модель
    Entry
    . В точке  мы получаем объект записи, который пользователь хочет изменить, и тему, связанную с этой записью.
    В блоке if
    , который выполняется для запроса GET, создается экземпляр
    EntryForm с аргументом instance=entry
    . Этот аргумент приказывает Django создать форму, заранее заполненную информацией из существующего объекта записи. Пользова- тель видит свои существующие данные и может отредактировать их.
    При обработке запроса POST передаются аргументы i n s t a n c e = e n t r y и data=request.POST
    , чтобы приказать Django создать экземпляр формы на основании информации существующего объекта записи, обновленный данными из request.POST
    . Затем проверяется корректность данных формы. Если данные корректны, следует вызов save()
    без аргументов . Далее происходит перена- правление на страницу темы , и пользователь видит обновленную версию от- редактированной им записи.

    418 Глава 19 • Учетные записи пользователей
    Шаблон edit_entry
    Шаблон edit_entry .html очень похож на new_entry .html
    :
    edit_entry.html
    {% extends "learning_logs/base.html" %}
    {% block content %}
    {{ topic }}
    Edit entry:


    {% csrf_token %}
    {{ form.as_p }}



    {% endblock content %}
    В точке  аргумент action отправляет форму функции edit_entry()
    для обработ- ки. Идентификатор записи включается как аргумент в тег
    {%
    url
    %}
    , чтобы функ- ция представления могла изменить правильный объект записи. Кнопка отправки данных создается с текстом, который напоминает пользователю, что он сохраняет изменения, а не создает новую запись .
    Создание ссылки на страницу edit_entry
    Теперь необходимо включить ссылку на страницу edit_entry в каждую тему на странице со списком тем:
    topic.html
    {% for entry in entries %}

  • {{ entry.date_added|date:'M d, Y H:i' }}
    {{ entry.text|linebreaks }}
    edit entry

  • После даты и текста каждой записи включается ссылка редактирования. Мы ис- пользуем шаблонный тег
    {%
    url
    %}
    для определения схемы URL из именованной схемы edit_entry и идентификатора текущей записи в цикле (
    entry.id
    ). Текст ссылки "edit entry"
    выводится после каждой записи на странице. На рис. 19.3 по- казано, как выглядит страница со списком тем с этими ссылками.
    Приложение Learning Log уже сейчас содержит бульшую часть необходимой функциональности. Пользователи могут добавлять темы и записи, а также чи- тать любые записи по своему усмотрению. В этом разделе мы реализуем систему

    Создание учетных записей пользователей 419
    Рис. 19.3. Каждая запись снабжается ссылкой для редактирования этой записи регистрации пользователей, чтобы любой желающий мог создать свою учетную запись в Learning Log и ввести собственный набор тем и записей.
    УПРАЖНЕНИЯ
    19-1 . Блог: создайте новый проект Django с именем Blog . Создайте в проекте приложе- ние с именем blogs и моделью BlogPost . Модель должна содержать такие поля, как title, text и date_added . Создайте суперпользователя для проекта и воспользуйтесь администра- тивным сайтом для создания пары коротких сообщений . Создайте домашнюю страницу, на которой выводятся все сообщения в хронологическом порядке .
    Создайте одну форму для создания новых сообщений и другую форму для редактирования существующих сообщений . Заполните формы и убедитесь в том, что они работают .
    Создание учетных записей пользователей
    В этом разделе мы создадим систему регистрации и авторизации пользователей, чтобы люди могли создать учетную запись, начать и завершать сеанс работы с при- ложением. Для всей функциональности, относящейся к работе с пользователями, будет создано отдельное приложение. Мы также слегка изменим модель
    Topic
    , чтобы каждая тема была связана с конкретным пользователем.
    Приложение users
    Начнем с создания нового приложения users командой startapp
    :
    (ll_env)learning_log$ python manage.py startapp users
    (ll_env)learning_log$ ls

    420 Глава 19 • Учетные записи пользователей
     db.sqlite3 learning_log learning_logs ll_env manage.py users
    (ll_env)learning_log$ ls users
     admin.py __init__.py migrations models.py tests.py views.py
    Эта команда создает новый каталог с именем users
    , структура которого повторяет структуру каталогов приложения learning_logs
    .
    Добавление пользователей в settings .py
    Новое приложение необходимо добавить в settings .py
    :
    settings.py
    INSTALLED_APPS = (
    # My apps
    'learning_logs',
    'users',
    )
    Django включает приложение users в общий проект.
    Включение URL-адресов из users
    Затем необходимо изменить корневой файл urls .py
    , чтобы он включал URL-адреса, написанные для приложения users
    :
    urls.py
    from django.conf.urls import include, url from django.contrib import admin urlpatterns = [
    url(r'^admin/', include(admin.site.urls)),
    url(r'^users/', include('users.urls', namespace='users')),
    url(r'', include('learning_logs.urls', namespace='learning_logs')),
    ]
    Добавим строку для включения файла urls .py из users
    . Эта строка будет соответ- ствовать любому URL-адресу, начинающемуся со слова users
    , — например, http://
    localhost:8000/users/login/. Также будет создано пространство имен 'users'
    , чтобы мы могли отличать URL-адреса, принадлежащие приложению learning_logs
    , от URL, принадлежащих приложению users
    Страница входа
    Начнем с реализации страницы входа. Мы воспользуемся стандартным представле- нием login
    , которое предоставляет Django, так что шаблон URL выглядит немного иначе. Создайте новый файл urls .py в каталоге learning_log/users/
    и добавьте в него следующий код:
    urls.py
    """Определяет схемы URL для пользователей"""

    Создание учетных записей пользователей 421
    from django.conf.urls import url
     from django.contrib.auth.views import login from . import views urlpatterns = [
    # Страница входа
     url(r'^login/$', login, {'template_name': 'users/login.html'},
    name='login'),
    ]
    Сначала импортируется представление login по умолчанию . Схема страницы входа соответствует URL http://localhost:8000/users/login/ . Когда Django чита- ет этот URL-адрес, слово users указывает, что следует обратиться к users/urls .py
    , а login сообщает о том, что запросы должны отправляться представлению login по умолчанию (обратите внимание: в аргументе представления используется login
    , а не views.login
    ). Так как мы не пишем собственную функцию представления, мы передаем словарь, который сообщает Django, где искать шаблон (сейчас мы его напишем). Этот шаблон будет частью приложения users
    , а не приложения learning_logs
    Шаблон login
    Когда пользователь запрашивает страницу входа, Django использует свое пред- ставление login по умолчанию, но мы все равно должны предоставить шаблон для этой страницы. В каталоге learning_log/users/
    создайте каталог с именем templates
    , а внутри него — еще один каталог с именем users
    . Вот как выглядит шаблон login .html
    , который должен находиться в learning_log/users/templates/
    users/
    :
    login.html
    {% extends "learning_logs/base.html" %}
    {% block content %}

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


    {% csrf_token %}

    {{ form.as_p }}





    {% endblock content %}
    Шаблон расширяет base .html
    , чтобы страница входа по оформлению и поведению была похожа на другие страницы сайта. Обратите внимание: шаблон в одном при- ложении может расширять шаблон из другого приложения.

    422 Глава 19 • Учетные записи пользователей
    Если у формы установлен атрибут errors
    , выводится сообщение об ошибке .
    В нем говорится, что комбинация имени пользователя и пароля не соответствует информации, хранящейся в базе данных.
    Мы хотим, чтобы представление обработало форму, поэтому аргументу action присваивается URL страницы входа . Представление отправляет форму шаблону, мы должны вывести форму  и добавить кнопку отправки данных . В точке  включается скрытый элемент формы 'next'
    ; аргумент value сообщает Django, куда перенаправить пользователя после успешно выполненного входа. В нашем случае пользователь возвращается обратно на домашнюю страницу.
    Создание ссылки на страницу входа
    Добавим ссылку на страницу входа в base .html
    , чтобы она присутствовала на каждой странице. Ссылка не должна отображаться, если пользователь уже прошел про- цедуру входа, поэтому она вкладывается в тег
    {%
    if
    %}
    :
    base.html
    Learning Log -
    Topics -

    {% if user.is_authenticated %}

    Hello, {{ user.username }}.
    {% else %}
     log in
    {% endif %}
    {% block content %}{% endblock content %}
    В системе аутентификации Django в каждом шаблоне доступна переменная user
    , в которой всегда присутствует атрибут is_authenticated
    : атрибут равен
    True
    , если пользователь прошел проверку, и
    False в противном случае. Это по- зволяет вам выводить разные сообщения для проверенных и непроверенных пользователей.
    В данном случае мы выводим приветствие для пользователей, выполнивших вход.
    У проверенных пользователей устанавливается дополнительный атрибут username
    , который обеспечит личную настройку приветствия и напомнит пользователю о том, что вход был выполнен. В точке  выводится ссылка на страницу входа для пользователей, которые еще не прошли проверку.
    Использование страницы входа
    Учетная запись пользователя уже создана; попробуем ввести данные и посмотрим, работает ли страница. Откройте страницу http://localhost:8000/admin/. Если вы все еще работаете с правами администратора, найдите ссылку выхода в заголовке и щелкните на ней.
    После выхода перейдите по адресу http://localhost:8000/users/login/. На экране должна появиться страница входа, похожая на рис. 19.4. Введите имя пользователя и пароль, заданные ранее, и вы снова должны оказаться на странице со списком.

    Создание учетных записей пользователей 423
    Рис. 19.4. Страница входа
    В заголовке страницы должно выводиться сообщение с указанием имени пользо- вателя.
    Выход
    Теперь необходимо предоставить пользователям возможность выхода из приложе- ния. Мы не будем строить отдельную страницу для выхода; пользователь просто щелкает на ссылке и возвращается к домашней странице. Мы определяем схему
    URL для ссылки выхода, пишем функцию представления и предоставляем ссылку выхода в base .html
    URL-адрес выхода
    Следующий код определяет схему URL для выхода, соответствующую URL
    http://localhost:8000/users/logout/. Файл users/urls .py выглядит так:
    urls.py
    urlpatterns = [
    # Страница входа
    # Страница выхода url(r'^logout/$', views.logout_view, name='logout'),
    ]
    Схема URL отправляет запрос функции logout_view()
    , имя которой выбрано так, чтобы оно отличалось от имени функции logout()
    , вызываемой из представления.
    (Проследите за тем, чтобы изменения вносились в файл users/urls .py
    , а не в файл learning_log/urls .py
    .)
    Функция представления logout_view()
    Функция logout_view()
    тривиальна: мы просто импортируем функцию Django logout()
    , вызываем ее, а затем возвращаем пользователя на домашнюю страницу.
    Откройте файл users/views .py и введите следующий код:

    424 Глава 19 • Учетные записи пользователей
    views.py
    from django.http import HttpResponseRedirect from django.core.urlresolvers import reverse
     from django.contrib.auth import logout def logout_view(request):
    """Завершает сеанс работы с приложением."""
     logout(request)
     return HttpResponseRedirect(reverse('learning_logs:index'))
    Мы импортируем функцию logout()
    из django.contrib.auth
    . В функции вы- зывается функция logout()
    , в аргументе которой должен передаваться объект запроса. Затем происходит возврат к домашней странице .
    Ссылка на представление выхода
    Теперь нужно создать ссылку для выхода. Мы включим ее в base .html
    , чтобы она была доступна на каждой странице, и включим в секцию
    {%
    if user.is_
    authenticated
    %}
    , чтобы ссылка была видна только пользователям, уже выпол- нившим вход:
    base.html
    {% if user.is_authenticated %}
    Hello, {{ user.username }}.
    log out
    {% else %}
    log in
    {% endif %}
    На рис. 19.5 изображена текущая домашняя страница так, как ее видит пользова- тель, выполнивший вход. Оформление страницы минимально, потому что сейчас нас в первую очередь интересует работа сайта. Когда необходимые функции зара- ботают, можно переходить к стилевому оформлению сайта и приданию ему более профессионального вида.
    Рис. 19.5. Домашняя страница с персональным приветствием и ссылкой для выхода

    Создание учетных записей пользователей 425
    Страница регистрации
    Теперь мы построим страницу для регистрации новых пользователей. Для этой цели мы используем класс Django
    UserCreationForm
    , но напишем собственную функцию представления и шаблон.
    URL-адрес регистрации
    Следующий код предоставляет шаблон URL для страницы регистрации — также в файле users/urls .py
    :
    urls.py
    urlpatterns = [
    # Страница входа
    # Страница регистрации url(r'^register/$', views.register, name='register'),
    ]
    Шаблон соответствует URL http://localhost:8000/users/register/ и отправляет за- просы функции register()
    , которую мы сейчас напишем.
    Функция представления register()
    Функция представления register()
    должна вывести пустую форму регистрации при первом запросе страницы регистрации, а затем обрабатывает заполненную форму регистрации при отправке данных. Если регистрация прошла успешно, функция также должна выполнить вход для нового пользователя. Включите сле- дующий код в users/views .py
    :
    views.py
    from django.shortcuts import render from django.http import HttpResponseRedirect from django.core.urlresolvers import reverse from django.contrib.auth import login, logout, authenticate from django.contrib.auth.forms import UserCreationForm def logout_view(request):
    def register(request):
    """Регистрирует нового пользователя."""
    if request.method != 'POST':
    # Display blank registration form.
     form = UserCreationForm()
    else:
    # Обработка заполненной формы.
     form = UserCreationForm(data=request.POST)
     if form.is_valid():
     new_user = form.save()
    # Выполнение входа и перенаправление на домашнюю страницу.

    426 Глава 19 • Учетные записи пользователей
     authenticated_user = authenticate(username=new_user.username,
    password=request.POST['password1'])
     login(request, authenticated_user)
     return HttpResponseRedirect(reverse('learning_logs:index'))
    context = {'form': form}
    return render(request, 'users/register.html', context)
    Сначала импортируется функция render()
    , после чего импортируются функции login()
    и authenticate()
    для выполнения входа пользователя, если регистраци- онная информация верна. Также импортируется класс
    UserCreationForm по умол- чанию. В функции register()
    мы проверяем, отвечает ли функция на запрос
    POST. Если нет, создается экземпляр
    UserCreationForm
    , не содержащий исходных данных . В случае ответа на запрос POST создается экземпляр
    UserCreationForm
    , основанный на отправленных данных . Мы проверяем, что данные верны ; в данном случае что имя пользователя содержит правильные символы, пароли совпадают, а пользователь не пытается вставить вредоносные конструкции в от- правленные данные.
    Если отправленные данные верны, мы вызываем метод save()
    формы для со- хранения имени пользователя и хеша пароля в базе данных . Метод save()
    возвращает только что созданный объект пользователя, который сохраняется в new_user
    После того как информация пользователя будет сохранена, мы выполняем вход; этот процесс состоит из двух шагов: сначала вызывается функция authenticate()
    с аргументом new_user.username и паролем . При регистрации пользователю предлагается ввести два совпадающих пароля; поскольку данные формы верны, мы знаем, что пароли совпадают, и можем использовать любой из них. В данном случае используется значение, связанное с ключом 'password1'
    в данных POST формы. Если имя пользователя и пароль верны, метод возвращает проверенный объект пользователя, который сохраняется в authenticated_user
    . Затем вызывает- ся функция login()
    с объектами request и authenticated_user

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

    , где привет- ствие в заголовке сообщает о том, что регистрация прошла успешно.
    Шаблон регистрации
    Шаблон страницы регистрации похож на шаблон страницы входа. Проследите за тем, чтобы он был сохранен в одном каталоге с login .html
    :
    register.html
    {% extends "learning_logs/base.html" %}
    {% block content %}

    {% csrf_token %}
    {{ form.as_p }}


    Создание учетных записей пользователей 427


    {% endblock content %}
    Мы снова используем метод as_p
    , чтобы инфраструктура Django могла правильно отобразить все поля формы, включая все сообщения об ошибках, если форма была заполнена неправильно.
    Создание ссылки на страницу регистрации
    Следующий шаг — добавление кода для вывода ссылки на страницу регистрации для любого пользователя, еще не выполнившего вход:
    base.html
    {% if user.is_authenticated %}
    Hello, {{ user.username }}.
    log out
    {% else %}
    register - log in
    {% endif %}
    Теперь пользователи, выполнившие вход, получат персональное приветствие и ссылку для выхода. Другие пользователи видят ссылку на страницу регистрации и ссылку для входа. Проверьте страницу регистрации, создав несколько учетных записей с разными именами пользователей.
    В следующем разделе доступ к некоторым страницам будет ограничен, чтобы страницы были доступны только для зарегистрированных пользователей. Также необходимо позаботиться о том, чтобы каждая тема принадлежала конкретному пользователю.
    ПРИМЕЧАНИЕ
    Такая система регистрации позволяет любому пользователю создать сколько угодно учетных за- писей Learning Log . Однако некоторые системы требуют, чтобы пользователь подтвердил свою заявку, отправляя сообщение электронной почты, на которое пользователь должен ответить .
    При таком подходе в системе будет создано меньше спамерских учетных записей, чем в простей- шей системе из нашего примера . Но пока вы только учитесь строить приложения, вполне нормаль- но тренироваться на упрощенной системе регистрации вроде используемой нами .
    УПРАЖНЕНИЯ
    19-2 . Учетные записи в блоге: добавьте систему аутентификации и регистрации в проект
    Blog, работа над которым началась в упражнении 19-1 (с . 419) . Проследите за тем, чтобы пользователь, выполнивший вход, видел свое имя где-то на экране, а незарегистрирован- ные пользователи видели ссылку на страницу регистрации .

    428 Глава 19 • Учетные записи пользователей
    Редактирование данных
    Пользователь должен иметь возможность вводить данные, принадлежащие только ему лично. Мы создадим систему, которая будет определять, какому пользователю принадлежат те или иные данные, и ограничивать доступ к страницам, чтобы поль- зователь мог работать только с принадлежащими ему данными.
    В этом разделе мы изменим модель
    Topic
    , чтобы каждая тема принадлежала кон- кретному пользователю. При этом также автоматически решается проблема с запи- сями, так как каждая запись принадлежит конкретной теме. Начнем с ограничения доступа к страницам.
    Ограничение доступа с использованием @login_required
    Django позволяет легко ограничить доступ к определенным страницам для поль- зователей, выполнивших вход, с помощью декоратора
    @login_required
    . Декоратор
    (decorator) представляет собой директиву, размещенную непосредственно перед определением функции, применяемую к функции перед ее выполнением и влия- ющую на поведение кода. Рассмотрим пример.
    Ограничение доступа к страницам тем
    Каждая тема будет принадлежать пользователю, поэтому только зарегистриро- ванные пользователи смогут запрашивать страницы тем. Добавьте следующий код в learning_logs/views .py
    :
    views.py
    from django.core.urlresolvers import reverse from django.contrib.auth.decorators import login_required from .models import Topic, Entry
    @login_required def topics(request):
    """Выводит все темы."""
    Сначала импортируется функция login_required()
    . Мы применяем login_
    required()
    как декоратор для функции представления topics()
    , для чего перед именем login_required()
    ставится знак
    @
    ; он сообщает Python, что этот код должен выполняться перед кодом topics()
    Код login_required()
    проверяет, выполнил ли пользователь вход, и Django вы- полняет код topics()
    только при выполнении этого условия. Если же пользователь не выполнил вход, он перенаправляется на страницу входа.
    Чтобы перенаправление работало, необходимо внести изменения settings .py и со- общить Django, где искать страницу входа. Добавьте следующий фрагмент в самый конец settings .py
    :

    Редактирование данных 429
    settings.py
    """
    Django settings for learning_log project
    # Мои настройки
    LOGIN_URL = '/users/login/'
    Когда пользователь, не прошедший проверку, запрашивает страницу, защищенную декоратором
    @login_required
    , Django отправляет пользователя на URL-адрес, определяемый
    LOGIN_URL
    в settings .py
    Чтобы протестировать эту возможность, завершите сеанс в любой из своих учетных записей и вернитесь на домашнюю страницу. Щелкните на ссылке
    Topics
    , которая должна направить вас на страницу входа. Выполните вход с любой из своих учет- ных записей, на домашней странице снова щелкните на ссылке
    Topics
    . На этот раз вы получите доступ к странице со списком тем.
    Ограничение доступа в Learning Log
    Django упрощает ограничение доступа к страницам, но вы должны решить, какие страницы следует защищать. Лучше сначала подумать, к каким страницам можно разрешить неограниченный доступ, а затем ограничить его для всех остальных страниц. Снять излишние ограничения несложно, причем это куда менее риско- ванно, чем оставлять действительно важные страницы без ограничения доступа.
    В приложении Learning Log мы оставим неограниченный доступ к домашней странице, странице регистрации и выхода. Доступ ко всем остальным страницам будет ограничен.
    Вот как выглядит файл learning_logs/views .py с декораторами
    @login_required
    , при- мененными к каждому представлению, кроме index()
    :
    views.py
    @login_required def topics(request):
    @login_required def topic(request, topic_id):
    @login_required def new_topic(request):
    @login_required def new_entry(request, topic_id):
    @login_required def edit_entry(request, entry_id):

    430 Глава 19 • Учетные записи пользователей
    Попробуйте обратиться к любой из этих страниц без выполнения входа: вы будете пере- направлены обратно на страницу входа. Кроме того, вы не сможете щелкать на ссылках на такие страницы, как new_topic
    . Но если ввести URL http://localhost:8000/new_topic/, вы будете перенаправлены на страницу входа. Ограничьте доступ ко всем URL-адресам, связанным с личными данными пользователей.
    Связывание данных с конкретными пользователями
    Теперь данные, отправленные пользователем, необходимо связать с тем пользо- вателем, который их отправил. Связь достаточно установить только с данными, находящимися на высшем уровне иерархии, а низкоуровневые данные последуют за ними автоматически. Например, в приложении Learning Log на высшем уровне находятся темы, а каждая запись связывается с некоторой темой. Если каждая тема принадлежит конкретному пользователю, мы сможем отследить владельца каждой записи в базе данных.
    Изменим модель
    Topic и добавим отношение внешнего ключа с пользователем.
    После этого необходимо провести миграцию базы данных. Наконец, необходимо изменить некоторые представления, чтобы в них отображались только данные, связанные с текущим пользователем.
    Изменение модели Topic
    В файле models .py изменяются всего две строки:
    models.py
    from django.db import models from django.contrib.auth.models import User class Topic(models.Model):
    """Тема, которую изучает пользователь"""
    text = models.CharField(max_length=200)
    date_added = models.DateTimeField(auto_now_add=True)
    owner = models.ForeignKey(User)
    def __str__(self):
    """Возвращает строковое представление модели."""
    return self.text class Entry(models.Model):
    Сначала модель
    User импортируется из django.contrib.auth
    . Затем в
    Topic до- бавляется поле owner
    , используемое в отношении внешнего ключа с моделью
    User
    Идентификация существующих пользователей
    При проведении миграции Django модифицирует базу данных, чтобы в ней хра- нилась связь между каждой темой и пользователем. Для выполнения миграции
    Django необходимо знать, с каким пользователем должна быть связана каждая существующая тема. Проще всего связать все существующие темы с одним поль-

    Редактирование данных 431
    зователем, например суперпользователем. Но для этого сначала необходимо узнать идентификатор этого пользователя.
    Просмотрим идентификаторы всех пользователей, созданных до настоящего мо- мента. Запустите сеанс оболочки Django и введите следующие команды:
    (venv)learning_log$ python manage.py shell

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

    >>> User.objects.all()
    [, , ]

    >>> for user in User.objects.all():
    ... print(user.username, user.id)
    ll_admin 1 eric 2 willie 3
    >>>
    В точке  в сеанс оболочки импортируется модель
    User
    . После этого просматрива- ются все пользователи, созданные до настоящего момента . В выходных данных перечислены три пользователя: ll_admin, eric и willie.
    В точке  перебирается список пользователей, и для каждого пользователя вы- водится его имя и идентификатор. Когда Django спросит, с каким пользователем связать существующие темы, мы используем один из этих идентификаторов.
    Миграция базы данных
    Зная значение идентификатора, можно провести миграцию базы данных.

    (venv)learning_log$ python manage.py makemigrations learning_logs

    You are trying to add a non-nullable field 'owner' to topic without a default;
    we can't do that (the database needs something to populate existing rows).

    Please select a fix:
    1) Provide a one-off default now (will be set on all existing rows)
    2) Quit, and let me add a default in models.py

    Select an option: 1

    Please enter the default value now, as valid Python
    The datetime and django.utils.timezone modules are available, so you can do e.g. timezone.now()

    >>> 1
    Migrations for 'learning_logs':
    0003_topic_owner.py:
    - Add field owner to topic
    Сначала выдается команда makemigrations
    . В ее выходных данных  Django со- общает, что мы пытаемся добавить обязательное поле (значения которого отличны от null) в существующую модель (
    topic
    ) без указания значения по умолчанию.
    Django предоставляет два варианта : мы можем либо указать значение по умол- чанию прямо сейчас, либо завершить выполнение программы и добавить значение по умолчанию в models .py
    . В точке  выбирается первый вариант. Тогда Django запрашивает значение по умолчанию .
    Чтобы связать все существующие темы с исходным административным пользо- вателем ll_admin
    , я ввел в точке

    идентификатор пользователя 1. Вы можете

    432 Глава 19 • Учетные записи пользователей использовать идентификатор любого из созданных пользователей; он не обязан быть суперпользователем. Django проводит миграцию базы данных, используя это значение, и создает файл миграции
    0003_topic_owner .py
    , добавляющий поле owner в модель
    Topic
    Теперь можно провести миграцию. Введите следующую команду в активной вир- туальной среде:
    (venv)learning_log$ python manage.py migrate
    Operations to perform:
    Synchronize unmigrated apps: messages, staticfiles
    Apply all migrations: learning_logs, contenttypes, sessions, admin, auth
    Running migrations:
    Rendering model states... DONE

    Applying learning_logs.0003_topic_owner...
    OK
    (venv)learning_log$
    Django применяет новую миграцию с результатом
    OK
    . Чтобы убедиться в том, что миграция сработала так, как и ожидалось, можно воспользоваться интерактивной оболочкой:

    >>> from learning_logs.models import Topic

    >>> for topic in Topic.objects.all():
    ... print(topic, topic.owner)
    Chess ll_admin
    Rock Climbing ll_admin
    >>>
    После импортирования
    Topic из learning_logs.models
     мы перебираем все су- ществующие темы, выводим каждую тему и имя пользователя, которому она при- надлежит . Как видите, сейчас каждая тема принадлежит пользователю ll_admin
    ПРИМЕЧАНИЕ
    Вместо миграции можно просто сбросить содержимое базы данных, но это приведет к потере всех существующих данных . Полезно научиться выполнять миграцию базы данных без нарушения це- лостности данных пользователей . Если вы хотите начать с новой базы данных, используйте ко- манду python manage .py flush для повторного построения структуры базы данных . Вам придется создать нового суперпользователя, а все данные будут потеряны .
    Ограничение доступа к темам
    В настоящее время пользователь, выполнивший вход, будет видеть все темы не- зависимо от того, под какой учетной записью он вошел. Сейчас мы изменим при- ложение, чтобы каждый пользователь видел только принадлежащие ему темы.
    Внесите следующее изменение в функцию topics()
    в файле views .py
    :
    views.py
    @login_required

    Редактирование данных 433
    def topics(request):
    """Выводит список тем."""
    topics = Topic.objects.filter(owner=request.user).order_by('date_added')
    context = {'topics': topics}
    return render(request, 'learning_logs/topics.html', context)
    Если пользователь выполнил вход, в объекте запроса устанавливается атрибут request.user с информацией о пользователе. Фрагмент кода
    Topic.objects.
    filter(owner=request.user)
    приказывает Django извлечь из базы данных только те объекты
    Topic
    , у которых атрибут owner соответствует текущему пользователю.
    Так как способ отображения не изменяется, изменять шаблон для страницы тем вообще не нужно.
    Чтобы увидеть, как работает этот способ, выполните вход в качестве пользователя, с которым связаны все существующие темы, и перейдите к странице со списком тем. На ней должны отображаться все темы. Теперь завершите сеанс и войдите снова с другой учетной записью. На этот раз страница должна быть пустой.
    Защита тем пользователя
    Никаких реальных ограничений на доступ к страницам еще не существует, по- этому любой зарегистрированный пользователь может опробовать разные URL
    (например, http://localhost:8000/topics/1/) и просмотреть страницы тем, которые ему удастся подобрать.
    Попробуйте сделать это. После входа с учетной записью суперпользователя ско- пируйте URL или запишите идентификатор в URL темы, после чего завершите сеанс и войдите снова от имени другого пользователя. Введите URL этой темы.
    Вам удастся прочитать все записи, хотя сейчас вы вошли под именем другого пользователя.
    Чтобы решить эту проблему, мы будем выполнять проверку перед получением запрошенных данных в функции представления topic()
    :
    views.py
    from django.shortcuts import render
     from django.http import HttpResponseRedirect, Http404
    from django.core.urlresolvers import reverse
    @login_required def topic(request, topic_id):
    """Выводит одну тему и все ее записи."""
    topic = Topic.objects.get(id=topic_id)
    # Проверка того, что тема принадлежит текущему пользователю.
     if topic.owner != request.user:
    raise Http404
    entries = topic.entry_set.order_by('-date_added')
    context = {'topic': topic, 'entries': entries}
    return render(request, 'learning_logs/topic.html', context)

    434 Глава 19 • Учетные записи пользователей
    Код 404 — стандартное сообщение об ошибке, которое возвращается в тех случаях, когда запрошенный ресурс не существует на сервере. В данном случае мы импор- тируем исключение
    Http404
    , которое будет выдаваться программой при запросе пользователем темы, которую ему видеть не положено. Получив запрос темы, перед отображением страницы мы убеждаемся в том, что пользователь этой темы является текущим пользователем приложения. Если тема не принадлежит текущему пользова- телю, выдается исключение
    Http404
    , а Django возвращает страницу с ошибкой 404.
    Пока при попытке просмотреть записи другого пользователя вы получите от Django сообщение «Страница не найдена». В главе 20 проект будет настроен так, чтобы пользователь видел полноценную страницу ошибки.
    Защита страницы edit_entry
    Страницы edit_entry используют URL-адреса в форме http://localhost:8000/edit_
    entry/entry_id/, где entry_id — число. Защитим эту страницу, чтобы никто не мог подобрать URL для получения доступа к чужим записям:
    views.py
    @login_required def edit_entry(request, entry_id):
    """Редактирует существующую запись."""
    entry = Entry.objects.get(id=entry_id)
    topic = entry.topic if topic.owner != request.user:
    raise Http404
    if request.method != 'POST':
    # Исходный запрос; форма заполняется данными текущей записи.
    Программа читает запись и тему, связанную с этой записью. Затем мы проверяем, совпадает ли владелец темы с текущим пользователем; при несовпадении выдается исключение
    Http404
    Связывание новых тем с текущим пользователем
    В настоящее время страница добавления новых тем несовершенна, потому что она не связывает новые темы с конкретным пользователем. При попытке добавить новую тему выдается сообщение об ошибке
    IntegrityError с уточнением learning_
    logs_topic.user_id may not be
    NULL
    . Django говорит, что при создании новой темы обязательно должно быть задано значение поля owner
    Проблема легко решается, потому что мы можем получить доступ к информации текущего пользователя через объект request
    . Добавьте следующий код, связыва- ющий новую тему с текущим пользователем:
    views.py

    Итоги 435
    @login_required def new_topic(request):
    """Определяет новую тему."""
    if request.method != 'POST':
    # Данные не отправлялись; создается пустая форма.
    form = TopicForm()
    else:
    # Отправлены данные POST; обработать данные.
    form = TopicForm(request.POST)
    if form.is_valid():
     new_topic = form.save(commit=False)
     new_topic.owner = request.user
     new_topic.save()
    return HttpResponseRedirect(reverse('learning_logs:topics'))
    context = {'form': form}
    return render(request, 'learning_logs/new_topic.html', context)
    При первом вызове form.save()
    передается аргумент commit=False
    , потому что новая тема должна быть изменена перед сохранением в базе данных . Атрибуту owner новой темы присваивается текущий пользователь . Наконец, мы вызываем save()
    для только что определенного экземпляра темы . Теперь тема содержит все обязательные данные, и ее сохранение пройдет успешно.
    Вы сможете добавить сколько угодно новых тем для любого количества разных пользователей. Каждому пользователю будут доступны только его собственные данные, какие бы операции он ни пытался выполнять: просмотр данных, ввод новых или изменение существующих данных.
    УПРАЖНЕНИЯ
    19-3 . Рефакторинг: в views .py есть два места, в которых программа проверяет, что пользо- ватель, связанный с темой, является текущим пользователем . Поместите код этой проверки в функцию с именем check_topic_owner() и вызовите эту функцию при необходимости .
    19-4 . Защита new_entry: пользователь может попытаться добавить новую запись в журнал другого пользователя, вводя URL-адрес с идентификатором темы, принадлежащей друго- му пользователю . Чтобы предотвратить подобные атаки, перед сохранением новой записи проверьте, что текущий пользователь является владельцем темы, к которой относится за- пись .
    19-5 . Защищенный блог: в проекте Blog примите меры к тому, чтобы каждое сообщение в блоге было связано с конкретным пользователем . Убедитесь в том, что чтение всех со- общений доступно всем пользователям, но только зарегистрированные пользователи могут создавать новые и редактировать существующие сообщения . В представлении, в котором пользователи редактируют сообщения, перед обработкой формы убедитесь в том, что ре- дактируемое сообщение принадлежит именно этому пользователю .
    Итоги
    В этой главе вы научились использовать формы для создания новых тем и за- писей, а также редактирования существующих данных. Далее мы перешли к ре- ализации системы учетных записей. Вы предоставили существующим пользова-

    436 Глава 19 • Учетные записи пользователей телям возможность начинать и завершать сеанс работы с приложением, а также научились использовать класс Django
    UserCreationForm для создания новых учетных записей.
    После создания простой системы аутентификации и регистрации пользователей вы ограничили доступ пользователей к некоторым страницам; для этого исполь- зовался декоратор
    @login_required
    . Затем данные были связаны с конкретными пользователями при помощи отношения внешнего ключа. Вы также узнали, как выполнить миграцию базы данных, когда миграция требует ввести данные по умолчанию.
    В последней части главы вы узнали, как ограничить состав данных, просматрива- емых пользователем, с использованием функций представления. Для чтения со- ответствующих данных использовался метод filter()
    , а владелец запрашиваемых данных сравнивался с текущим пользователем.
    Не всегда бывает сразу понятно, какие данные должны быть доступны всем поль- зователям, а какие данные следует защищать, но этот навык приходит с практикой.
    Решения, принятые нами в этой главе для защиты данных пользователей, наглядно показывают, почему при построении проекта желательно работать в команде: если кто-то просматривает код вашего проекта, это повышает вероятность выявления плохо защищенных областей.
    К настоящему моменту мы построили полностью функционирующий проект, ра- ботающий на локальной машине. В последней главе мы доработаем оформление приложения Learning Log, чтобы оно выглядело более привлекательно. Также про- ект будет развернут на сервере, чтобы любой пользователь с доступом к Интернету мог зарегистрироваться и создать учетную запись.

    20
    Оформление и развертывание приложения
    Приложение Learning Log уже вполне работоспособно, но оно не имеет стилевого оформления и работает только на локальной машине. В этой главе мы определим для проекта простое, но профессиональное оформление, а затем развернем его на сервере, чтобы любой желающий мог создать учетную запись.
    Для стилевого оформления будет использоваться библиотека Bootstrap — набор инструментов для оформления веб-приложений, с которыми они будут выглядеть профессионально на любых современных устройствах, от большого монитора с пло- ским экраном до смартфона. Для этого мы воспользуемся приложением django- bootstrap3
    , а вы заодно потренируетесь в использовании приложений, созданных другими разработчиками Django.
    Для развертывания Learning Log будет использоваться Heroku — сайт, по- зволяющий загрузить ваш проект на один из его серверов, чтобы сделать его доступным для любого пользователя с подключением к Интернету. Также мы начнем пользоваться системой контроля версий Git для отслеживания изменений в проекте.
    Когда работа с Learning Log будет завершена, вы будете уметь разрабатывать про- стые веб-приложения, придавать им качественный внешний вид и развертывать их на работающих серверах. Также по мере накопления опыта вы научитесь поль- зоваться ресурсами с материалами более высокого уровня.
    Оформление приложения Learning Log
    До сих пор мы намеренно игнорировали оформление приложения, чтобы сосредо- точиться на его функциональности. И это вполне разумный подход к разработке, потому что приложение приносит пользу только в том случае, если оно работает.
    Конечно, когда приложение начинает работать, оформление выходит на первый план, чтобы пользователи захотели работать с ним.
    В этом разделе я кратко опишу приложение django-bootstrap3
    и покажу, как ин- тегрировать его в проект и подготовить к развертыванию.

    438 Глава 20 • Оформление и развертывание приложения
    Приложение django-bootstrap3
    Для интеграции Bootstrap в наш проект будет использоваться приложение django- bootstrap3
    . Это приложение загружает необходимые файлы Bootstrap, размещает их в правильных каталогах проекта и предоставляет доступ к стилевым директивам в шаблонах проекта.
    Чтобы установить django-bootstrap3
    , введите следующую команду в активной виртуальной среде:
    (ll_env)learning_log$ pip install django-bootstrap3
    Successfully installed django-bootstrap3
    Затем необходимо добавить следующий код для включения django-boostrap3
    в список
    INSTALLED_APPS
    в файле settings .py
    :
    settings.py
    INSTALLED_APPS = (
    'django.contrib.staticfiles',
    # Сторонние приложения 'bootstrap3',
    # Мои приложения 'learning_logs',
    'users',
    )
    Создайте новую секцию для приложений, созданных другими разработчиками, и включите в нее запись 'bootstrap3'
    . Обычно приложения должны включаться в
    INSTALLED_APPS
    ,но для надежности прочитайте инструкции по установке кон- кретного приложения.
    Приложение django-bootstrap3
    должно включать jQuery — библиотеку JavaScript, которая содержит некоторые интерактивные элементы, предоставляемые шабло- ном Bootstrap. Добавьте следующий код в конец settings .py
    :
    settings.py
    # Мои настройки
    LOGIN_URL = '/users/login/'
    # Настройки django-bootstrap3
    BOOTSTRAP3 = {
    'include_jquery': True,
    }
    Этот фрагмент избавляет вас от необходимости загружать библиотеку jQuery и раз- мещать ее в правильном каталоге вручную.

    Оформление приложения Learning Log 439
    Использование Bootstrap для оформления Learning Log
    По сути Bootstrap представляет собой большой набор инструментов стилевого оформления. Также библиотека содержит ряд шаблонов, которые можно при- менить к проекту для формирования общего стиля. Если вы только начинаете работать с Bootstrap, вам будет намного проще воспользоваться этими шаблона- ми, чем использовать отдельные инструменты оформления. Чтобы просмотреть шаблоны, предоставляемые Bootstrap, перейдите в раздел
    Getting
    Started на сайте
    http://getbootstrap.com/; прокрутите страницу до заголовка
    Examples и найдите раздел
    Navbars in action
    . Мы воспользуемся шаблоном
    Static top navbar
    , который предоставляет простую панель навигации у верхнего края, заголовок страницы и контейнер для ее содержимого.
    На рис. 20.1 показано, как будет выглядеть домашняя страница после применения шаблона Bootstrap к base .html и незначительного изменения index .html
    Рис. 20.1. Домашняя страница Learning Log
    Теперь вы знаете, к какому результату мы стремимся, и вам будет проще понять дальнейшие пояснения.
    Изменение base .html
    Шаблон base .html необходимо изменить так, чтобы в нем был задействован шаблон
    Bootstrap. Новая версия base .html будет представлена в несколько этапов.

    440 Глава 20 • Оформление и развертывание приложения
    Определение заголовков HTML
    Первое изменение в base .html
    : заголовки HTML определяются в файле, чтобы при открытии страницы Learning Log в строке заголовка браузера выводилось имя сайта. Также будут добавлены некоторые требования для использования Bootstrap в шаблонах. Удалите все содержимое base .html и замените его следующим кодом:
    base.html

    {% load bootstrap3 %}


    1   ...   42   43   44   45   46   47   48   49   ...   53


    написать администратору сайта