Рубрики
Без рубрики

Учебник по веб-приложениям Django

Очень практичный учебник, в котором описываются шаги и код для создания веб-приложения Python с использованием фреймворка Django

Автор оригинала: Roberto García.

Рекомендательные приложения , разработанные с использованием Django , в том числе на данный момент приложение MyRestaurants.Проект разрабатывается в соответствии с Гибким Поведенческим подходом к разработке . Вы также можете следить за учебником через видео , доступные из плейлиста YouTube Django Web Project Tutorial :

Исходный код для этого проекта доступен по адресу: https://github.com/rogargon/myrecommendations

Непрерывная интеграция (CI) с использованием Travis:

Непрерывное развертывание (CD) в Heroku:

Прежде всего, установите последнюю версию Python из downloads и pipenv , чтобы помочь вам управлять зависимостями и виртуальными средами.

Затем создайте папку для нового проекта, в нашем случае проект называется “мои рекомендации”.:

$ mkdir myrecommendations

$ cd myrecommendations

Попав в папку мои рекомендации , активируйте виртуальную среду piper nv, чтобы упорядочить пакеты Python для вашего проекта, и начните с установки Django. Затем создайте новый проект Django:

$ pipenv shell

$ pipenv install Django

$ django-admin startproject myrecommendations .

В myrecommendations/settings.py , просмотрите настройки базы данных. Например, для базы данных SQLite они должны быть:

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
    }
}

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

$ python manage.py migrate

Команда “Миграция” смотрит на INSTALLED_APPS, определенные в ‘settings.py” и создает все необходимые таблицы базы данных в соответствии с настройками базы данных.

Чтобы завершить создание проекта, определите пользователя admin:

$ python manage.py createsuperuser

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

$ python manage.py startapp myrestaurants

Затем добавьте “мои рестораны” в список INSTALLED_APPS в myrecommendations/settings.py :

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

Теперь у нас есть первоначальный проект и приложение Django, которые мы начнем наполнять функциональностью.

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

Следовательно, и следуя подходу BDD, сначала мы определяем предполагаемые функции :

  • Регистрация Ресторана
  • Зарегистрировать Блюдо
  • Список Последних Ресторанов
  • Вид на Ресторан
  • Посмотреть Блюдо
  • Обзор ресторана
  • Редактировать Ресторан
  • Редактировать Блюдо

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

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

Для того, чтобы [достичь некоторой ценности для бизнеса], Как [тип заинтересованных сторон], Я хочу [некоторую новую функцию системы].

Результатом является следующий список файлов функций с соответствующим содержимым в папке features/ :

  • register_restaurant.feature Feature : Зарегистрируйте ресторан Для того, чтобы отслеживать рестораны, которые я посещаю, Как пользователь Я хочу зарегистрировать ресторан вместе с его местоположением и контактными данными.
  • register_dish.feature |/Feature : Зарегистрировать блюдо Для того, чтобы отслеживать блюда, которые я ем, Как пользователь я хочу зарегистрировать блюдо в соответствующем ресторане вместе с его деталями. list_restaurants.функция |/Функция
  • : Список ресторанов Для того, чтобы держать себя в курсе зарегистрированных ресторанов, Как пользователь |/я хочу перечислить последние 10 зарегистрированных ресторанов. view_restaurant.feature |/Feature : Просмотр ресторана Для того, чтобы знать о ресторане,
  • Как пользователь я хочу просмотреть детали ресторана, включая все его блюда и отзывы. view_dish.feature |/Feature : Просмотр блюда Для того, чтобы знать о блюде, Как пользователь я хочу
  • просмотреть сведения о зарегистрированном блюде. review_restaurant.feature |/Feature : Регистрация отзыва Для того, чтобы поделиться своим мнением о ресторане, Как пользователь, я хочу зарегистрировать отзыв с рейтингом и необязательным комментарием о ресторане. edit_restaurant.feature
  • |/Feature : Редактировать ресторан Для того, чтобы обновлять мои предыдущие регистры о ресторанах, Как пользователь |/я хочу редактировать созданный мной реестр ресторанов. edit_dish.feature |/Feature : Редактировать блюдо Для того, чтобы
  • обновлять мои предыдущие регистры о блюдах, Как пользователь |/я хочу отредактировать созданный мной регистр блюд.

Инструменты

Чтобы облегчить описание сценариев функций, подключая их к коду Python, который проверяет, удовлетворяются ли сценарии приложением, мы будем использовать синтаксис Gherkin и инструмент Behavior.

Чтобы установить Поведение:

$ pipenv install behave

Кроме того, чтобы можно было вывести браузер из теста и таким образом проверить, следует ли приложение ожидаемому поведению с точки зрения конечного пользователя, мы также будем использовать Splinter. Его можно установить с помощью следующей команды:

$ pipenv install splinter

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

Если предположить, что Chrome уже установлен на вашем компьютере, единственным требованием для его использования для автоматического тестирования является ChromeDriver, доступный для Windows, Linux и Mac из https://sites.google.com/a/chromium.org/chromedriver/downloads

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

apt-get update
apt-get install chromedriver

Или Brew на OSX:

brew update
brew tap homebrew/cask
brew cask install chromedriver

Или Шоколадный на Windows:

choco install chromedriver

Тестирование с Firefox вместо Chrome

В качестве альтернативы, для Firefox, установите браузер:

sudo apt-get update
sudo apt-get install firefox

Затем загрузите драйвер Gecko и распакуйте его из https://github.com/mozilla/geckodriver/releases/

Наконец, добавьте драйвер геккона в свой путь:

export PATH=$PATH:/your/path/to/geckodriver

И настройте среду тестирования в environment.py , как описано далее, использовать firefox вместо chrome .

Окружающая среда

После установки всех необходимых инструментов для BDD нам также необходимо настроить среду тестирования. В данном случае приложение Django мой ресторан.

Мы делаем это в файле в папке features/ под названием environment.py :

import os
import django
from behave.runner import Context
from django.contrib.staticfiles.testing import StaticLiveServerTestCase
from django.core.management import call_command
from django.shortcuts import resolve_url
from django.test.runner import DiscoverRunner
from splinter.browser import Browser

os.environ["DJANGO_SETTINGS_MODULE"] = "myrecommendations.settings"

class ExtendedContext(Context):
    def get_url(self, to=None, *args, **kwargs):
        return self.test.live_server_url + (
            resolve_url(to, *args, **kwargs) if to else '')

def before_all(context):
    django.setup()
    context.test_runner = DiscoverRunner()
    context.test_runner.setup_test_environment()
    context.browser = Browser('chrome', headless=True)

def before_scenario(context, scenario):
    context.test_runner.setup_databases()
    object.__setattr__(context, '__class__', ExtendedContext)
    context.test = StaticLiveServerTestCase
    context.test.setUpClass()

def after_scenario(context, scenario):
    context.test.tearDownClass()
    del context.test
    call_command('flush', verbosity=0, interactive=False)

def after_all(context):
    context.test_runner.teardown_test_environment()
    context.browser.quit()
    context.browser = None

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

  • Перед всеми тестами : настройка Django, подготовка его к тестированию и сеанс браузера на основе PhantomJS для работы в качестве пользователя.
  • Перед каждым сценарием : инициализируется база данных Django вместе с контекстом, который будет передан в реализацию каждого шага сценария со всеми данными о текущем состоянии приложения.
  • После каждого сценария : база данных Django уничтожается, поэтому следующий сценарий начнется с чистого. Таким образом, каждый сценарий не зависит от предыдущих, и помех можно избежать.
  • После всех тестов : среда тестирования уничтожается вместе с браузером, используемым для тестирования.

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

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

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

Особенность: Регистрация ресторана

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

Файл функций register_restaurant.feature в настоящее время выглядит следующим образом:

Feature: Register Restaurant
  In order to keep track of the restaurants I visit
  As a user
  I want to register a restaurant together with its location and contact details

Мы начнем детализировать его, добавив сценарии с помощью заинтересованных сторон. Сценарии описывают конкретные ситуации использования функции. Сценарии описываются в терминах предварительных условий (Givens), событий, связанных с указанной функцией (Whens), и результатов (Thens).

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

  Background: There is a registered user
    Given Exists a user "user" with password "password"

  Scenario: Register just restaurant name
    Given I login as user "user" with password "password"
    When I register restaurant
      | name        |
      | The Tavern  |
    Then I'm viewing the details page for restaurant by "user"
      | name        |
      | The Tavern  |
    And There are 1 restaurants

После определения сценария мы можем запустить процесс BDD, который подразумевает, что мы начинаем кодировать, когда тест завершается неудачей. Чтобы запустить тест BDD, мы должны ввести текст из среды Python, в которой было установлено поведение. Из корня проекта:

$ behave

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

You can implement step definitions for undefined steps with these snippets:

@given(u'Exists a user "user" with password "password"')
def step_impl(context):
    raise NotImplementedError(u'STEP: Given Exists a user "user" with password "password"')

@given(u'I login as user "user" with password "password"')
def step_impl(context):
    raise NotImplementedError(u'STEP: Given I login as user "user" with password "password"')
    
@when(u'I register restaurant')
def step_impl(context):
    raise NotImplementedError(u'STEP: When I register restaurant')

@then(u'I\'m viewing the details page for restaurant by "user"')
def step_impl(context):
    raise NotImplementedError(u'STEP: Then I\'m viewing the details page for restaurant by "user"')

@then(u'There are 1 restaurants')
def step_impl(context):
    raise NotImplementedError(u'STEP: Then There are 1 restaurants')

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

Шаги аутентификации

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

from behave import *

use_step_matcher("parse")

@given('Exists a user "{username}" with password "{password}"')
def step_impl(context, username, password):
    from django.contrib.auth.models import User
    User.objects.create_user(username=username, email='user@example.com', password=password)

@given('I login as user "{username}" with password "{password}"')
def step_impl(context, username, password):
    context.browser.visit(context.get_url('/accounts/login/?next=/myrestaurants/'))
    form = context.browser.find_by_tag('form').first
    context.browser.fill('username', username)
    context.browser.fill('password', password)
    form.find_by_value('login').first.click()

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

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

Второй шаг реализует поведение пользователя при входе в систему. Браузер, созданный в environment.py и передается на шаг через параметр контекста, используется для перехода к представлению входа в систему, заполнения входных данных формы входа с именами “имя пользователя” и “пароль” соответствующими значениями и, наконец, нажмите кнопку “войти”.

Чтобы поддержать это поведение, мы сначала свяжем представления входа, а также выхода из системы с представлениями django.contrib.auth.views в файле URL-адресов проекта, myrecommendations/urls.py :

from django.urls import path
from django.contrib import admin
from django.contrib.auth import views

urlpatterns = [
    path('admin/', admin.site.urls),
    path('accounts/login/', views.LoginView.as_view(), name='login'),
    path('accounts/logout/', views.LogoutView.as_view(), name='logout'),
]

И создайте шаблон формы входа в систему, как и ожидалось в представлении входа в систему Django по умолчанию в registration/login.html . Однако, чтобы Django мог находить шаблоны, мы сначала создаем папку templates в корне проекта, а другую-в приложении myrestaurants, myrestaurants/templates . Затем мы регистрируем их в качестве папок шаблонов по умолчанию в myrecommendations/settings.py определение “DIR” в ШАБЛОНАХ, как описано ниже:

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [os.path.join(BASE_DIR, 'templates')],
        'APP_DIRS': True,
        ...

Затем мы можем определить форму входа в templates/registration/login.html :


MyRecommendations Login Form

    {% if form.errors %}
        

Your username and password didn't match. Please try again.

{% endif %}
{% csrf_token %}
{{ form.username.label_tag }} {{ form.username }}
{{ form.password.label_tag }} {{ form.password }}

С помощью этого мы реализуем первые два шага в первом сценарии register_restaurant.feature|/. Если мы снова запустим behavior , мы получим следующий вывод, который показывает, что первые два шага реализованы, в то время как последние три еще не выполнены:

  Scenario: Register just restaurant name                 # features/register_restaurant.feature:9
    Given Exists a user "user" with password "password"   # features/steps/authentication.py:6 0.301s
    Given I login as user "user" with password "password" # features/steps/authentication.py:12 1.207s
    When I register restaurant                            # None
      | name       |
      | The Tavern |
    Then I'm viewing the details page for restaurant      # None
      | name       |
      | The Tavern |
    And There are 1 restaurants                           # None

  0 features passed, 1 failed, 7 skipped
  0 scenarios passed, 1 failed, 0 skipped
  2 steps passed, 0 failed, 0 skipped, 3 undefined

Шаг Регистрации Ресторана

Неопределенные шаги связаны с функцией Register Restaurant и реализованы в features/steps/register_restaurant.py :

from behave import *
import operator
from functools import reduce
from django.db.models import Q

use_step_matcher("parse")

@when(u'I register restaurant')
def step_impl(context):
    for row in context.table:
        context.browser.visit(context.get_url('myrestaurants:restaurant_create'))
        form = context.browser.find_by_tag('form').first
        for heading in row.headings:
            context.browser.fill(heading, row[heading])
        form.find_by_value('Submit').first.click()

@then(u'I\'m viewing the details page for restaurant by "{user}"')
def step_impl(context, user):
    q_list = [Q((attribute, context.table.rows[0][attribute])) for attribute in context.table.headings]
    from myrestaurants.models import Restaurant
    restaurant = Restaurant.objects.filter(reduce(operator.and_, q_list)).get()
    assert context.browser.url == context.get_url(restaurant)

@then(u'There are {count:n} restaurants')
def step_impl(context, count):
    from myrestaurants.models import Restaurant
    assert count == Restaurant.objects.count()

Первый шаг для каждой строки таблицы, предоставленной в качестве входных данных шага, переходит в представление myrestaurants restaurant_create и заполняет отображаемые входные данные формы соответствующими данными строк, идентифицированными заголовками таблиц. Например, если указано имя создаваемого ресторана, соответствующий ввод с именем “имя” заполняется в соответствующей ячейке таблицы. Наконец, после заполнения всех предоставленных входных данных формы, она будет отправлена.

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

Третий register_restaurant.py шаг реализации проверяет, что в настоящее время зарегистрировано указанное количество ресторанов.

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

Прежде всего, мы реализуем модель. На данный момент, учитывая требования в функции Регистрации ресторана, нам просто нужно текстовое поле для имени. Следовательно, мы можем добавить в myrestaurants/models.py :

from django.db import models

class Restaurant(models.Model):
    name = models.CharField(max_length=120)

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

$ python manage.py makemigrations myrestaurants

В результате создается первая миграция модели данных для приложения myrestaurants:

Migrations for 'myrestaurants':
  myrestaurants/migrations/0001_initial.py:
    - Create model Restaurant

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

$ python manage.py migrate

Теперь мы можем реализовать представление “мои рестораны:restaurant_create”, к которому браузер переходит на первом шаге, реализованном в register_restaurant.py . Он определен в myrestaurants/urls.py и ссылка на URL-адрес “/регистрация” в “/мои рестораны”:

from django.urls import path
from django.views.generic.edit import CreateView
from myrestaurants.forms import RestaurantForm
from myrestaurants.models import Restaurant

app_name = "myrestaurants"

urlpatterns = [
    # Register a restaurant, from: /myrestaurants/create
    path('restaurants/create',
        CreateView.as_view(
            model=Restaurant,
            template_name='myrestaurants/form.html',
            form_class=RestaurantForm),
        name='restaurant_create'),
]

Чтобы опубликовать URL-адреса и представления, определенные в myrestaurants/urls.py и сделать их доступными из проекта, они должны быть включены в файл глобальных URL-адресов myrecommendations/urls.py :

from django.contrib import admin
from django.urls import path, include
from django.contrib.auth import views
from django.views.generic import RedirectView

urlpatterns = [
    path('', RedirectView.as_view(pattern_name='myrestaurants:restaurant_list'), name='home'),
    path('admin/', admin.site.urls),
    path('myrestaurants/', include('myrestaurants.urls', namespace='myrestaurants')),
    path('accounts/login/', views.LoginView.as_view(), name='login'),
    path('accounts/logout/', views.LogoutView.as_view(), name='logout'),
]

Таким образом, представления, определенные для приложения myrestaurants, будут доступны из “/my restaurants/… “и их имена в пространстве имен “myrestaurants”.

Текущее представление-это представление модели Django для создания новых объектов модели, a Create View . Он связан с рестораном, поскольку он будет создавать экземпляры этой модели, а также требует отображения формы и шаблона, в котором будет отображаться форма.

Django предоставляет класс ModelForm для автоматической реализации форм для создания и обновления сущностей модели. Для создания ресторанов нам требуется Ресторан из подкласса ModelForm, подобный тому, который определен в myrestaurants/forms.py :

from django.forms import ModelForm
from myrestaurants.models import Restaurant

class RestaurantForm(ModelForm):
    class Meta:
        model = Restaurant
        exclude = ()

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

Формы отображаются с помощью шаблона, который их отображает. Таким образом, мы начнем определять шаблоны для приложения myrestaurants в myrestaurants/templates/myrestaurants .

Первый-это базовый шаблон, который определяет общую структуру для всех шаблонов приложений, myrestaurants/templates/myrestaurants/base.html :



   {% block title %}MyRestaurants by MyRecommentdations{% endblock %}




{% block content %} {% if error_message %}

{{ error_message }}

{% endif %} {% endblock %}

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

В myrestaurants/templates/myrestaurants/form.html шаблон-это шаблон, расширяющий базовый, который отображает форму в блоке содержимого:

{% extends "myrestaurants/base.html" %}

{% block content %}
{% csrf_token %} {{ form.as_table }}
{% endblock %}

Благодаря этому мы реализовали все необходимое для поддержки шага “Я регистрирую ресторан”. Если мы выполним behavior , теперь пройдет 3 шага, и следующее сообщение об ошибке будет выводом для неудачного шага:

ImproperlyConfigured: No URL to redirect to.  Either provide a url or define a get_absolute_url method on the Model.

Подробности о ресторане Шаг

Это потому, что Джанго не знает, что делать после создания ресторана. Поведение по умолчанию заключается в перенаправлении пользователя на представление, отображающее вновь созданный объект модели, и способ определения этого URL-адреса заключается в вызове метода get_absolute_url для модели. Таким образом, мы добавим в myrestaurants/model.py :

from django.db import models
from django.urls.base import reverse

class Restaurant(models.Model):
    name = models.TextField()

    def get_absolute_url(self):
        return reverse('myrestaurants:restaurant_detail', kwargs={'pk': self.pk})

Мы не исправляем URL-адрес, но возвращаем URL-адрес, связанный с представлением, ответственным за отображение сведений о ресторане. URL-адрес строится с использованием идентификатора отображаемой сущности, который передается в качестве параметра kwargs.

Теперь нам нужно определить это представление в myrestaurants/urls.py путем добавления нового шаблона URL-адреса:

    ...,
    # Restaurant details, /myrestaurants/1
    path('restaurants/',
        DetailView.as_view(
            model=Restaurant,
            template_name='myrestaurants/restaurant_detail.html'),
        name='restaurant_detail'),
]

Это соответствует представлению класса Django с именем DetailView , ответственному за отображение экземпляров связанной модели с использованием предоставленного шаблона.

Шаблон также расширяет базовый и на данный момент просто отображает связанный экземпляр ресторана, предоставленный детализированным представлением шаблону myrestaurants/templates/myrestaurants/restaurant_detail.html :

{% extends "myrestaurants/base.html" %}
{% block content %}

{{ restaurant.name }}

{% endblock %}

На этом завершается реализация первого сценария features/register_restaurant.feature . Если мы снова запустим behavior , мы получим, что все шаги в сценарии прошли:

  Scenario: Register just restaurant name                 # features/register_restaurant.feature:9
    Given Exists a user "user" with password "password"   # features/steps/authentication.py:6 0.313s
    Given I login as user "user" with password "password" # features/steps/authentication.py:12 0.859s
    When I register restaurant                            # features/steps/register_restaurant.py:8 0.382s
      | name       |
      | The Tavern |
    Then I'm viewing the details page for restaurant      # features/steps/register_restaurant.py:18 0.013s
      | name       |
      | The Tavern |
    And There are 1 restaurants                           # features/steps/register_restaurant.py:26 0.001s

1 feature passed, 0 failed, 7 skipped
1 scenario passed, 0 failed, 0 skipped
5 steps passed, 0 failed, 0 skipped, 0 undefined

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

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

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

Поле изображения-это своего рода поле в модели данных, которое позволяет связывать изображения с объектами модели и хранить их.

Прежде всего, необходимо установить подушку библиотеки образов Python. Следуйте: http://pillow.readthedocs.org/en/latest/installation.html

Затем, в myrecommendations/settings.py добавить:

MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
MEDIA_URL = '/media/'

И в myrecommendations/urls.py, добавить в конце:

from django.conf import settings
from django.views.static import serve

urlpatterns += [
    path('media/', serve, {'document_root': settings.MEDIA_ROOT, })
]

Наконец, в myrestaurants/моделях.py добавьте поле Image в класс Dish для хранения изображений блюд:

  image = models.ImageField(upload_to="myrestaurants", blank=True, null=True)

Затем это поле можно использовать в шаблонах для отображения изображений, например в dish_detail.html шаблон для добавления в myrestaurants/шаблоны/myrestaurants:

{% extends "myrestaurants/base.html" %}

{% block content %}

{{ dish.name }} {% if user == dish.user %} (edit) {% endif %}

{{ dish.description }}

{% if dish.image %}

{% endif %}

Served by {{ dish.restaurant.name}}

{% endblock %} {% block footer %} Created by {{ dish.user }} on {{ dish.date }} {% endblock %}

Также важно при редактировании поля изображения с помощью форм добавить соответствующую кодировку, которая будет использоваться при загрузке изображения. Для этого отредактируйте form.html и включить соответствующий атрибут enctype :

{% extends "myrestaurants/base.html" %}

{% block content %}

{% csrf_token %} {{ form.as_table }}
{% endblock %}

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

$ python manage.py makemigrations myrestaurants

$ python manage.py migrate

В противном случае вам нужно будет удалить файл базы данных и начать с нуля.

Тестирование метода средней оценки ресторана с использованием модульных тестов в myrestaurants/tests.py :

from django.contrib.auth.models import User
from django.test import TestCase
from models import RestaurantReview, Restaurant

class RestaurantReviewTestCase(TestCase):
    def setUp(self):
        trendy = Restaurant.objects.create(name="Trendy Restaurant")
        user1 = User.objects.create(username="user1")
        user2 = User.objects.create(username="user2")
        user3 = User.objects.create(username="user3")
        RestaurantReview.objects.create(rating=3, comment="Average...", restaurant=trendy, user=user1)
        RestaurantReview.objects.create(rating=5, comment="Excellent!", restaurant=trendy, user=user2)
        RestaurantReview.objects.create(rating=1, comment="Really bad!", restaurant=trendy, user=user3)
        Restaurant.objects.create(name="Unknown Restaurant")

    def test_average_3reviews(self):
        """The average review for a restaurant with 3 reviews is properly computed"""
        restaurant = Restaurant.objects.get(name="Trendy Restaurant")
        self.assertEqual(restaurant.averageRating(), 3)

    def test_average_no_review(self):
        """The average review for a restaurant without reviews is 0"""
        restaurant = Restaurant.objects.get(name="Unknown Restaurant")
        self.assertEqual(restaurant.averageRating(), 0)

Для выполнения тестов:

$ python manage.py test