Внимание! Если у вас возникли проблемы с выполнением предыдущего этапа, то вы сможете зайти в соответствующий урок, скачать архив предыдущего этапа, инсталлировать его, и начать этот урок именно с того самого места, где закончился предыдущий!
Содержание курса:
Наш проект близится к завершению, от которого нас отделяет буквально несколько последних шагов.
Самая важная функциональная часть, на которую приходится все основные операции с базой данных, была выполнена в 5-м уроке. Т.е. по сути, вся бизнес-логика у нас уже есть и нам остаётся только добавить пользовательский интерфейс для отображения данных БД и их изменения.
Для разминки давайте сначала сделаем то, что уже хорошо умеем - скопируем из шаблона в папку templates/shop последние 3 страницы, которые там ещё остались: shop.html, shop-details.html и cart.html. И попробуем открыть их в проекта.
Для этого прежде всего уберём из них то, что уже есть в базовом шаблоне. Далее добавим в конфигуратор shop/urls.py ссылки на эти страницы.
urlpatterns = [
path('fill-database/', views.fill_database, name='fill_database'),
path('', TemplateView.as_view(template_name='shop/shop.html'), name='shop'),
path('cart_view/',
TemplateView.as_view(template_name='shop/cart.html'), name='cart_view'),
path('detail/<int:pk>/',
TemplateView.as_view(template_name='shop/shop-details.html'),
name='shop_detail')
]
Кстати, обратите внимание: здесь показаны не только ссылки, но и сами вью! Перед нами пример того редкого случая, когда вью можно не импортировать из модуля views.py. Дело в том, что template_name (единственно необходимый параметр для TemplateView) может быть указан в методе as_view() как kwargs-аргумент. И тогда вью “запустится” прямо из конфигуратора shop/urls.py!
И последний штрих - добавим в главное меню ссылки на страницы SHOP и CART, чтобы сразу же проверить, не допустили ли мы ошибок в вёрстке при встраивании базового шаблона. Ссылку на страницу детализации добавлять ещё некуда. Поэтому, для проверки этой страницы на отсутствие ошибок, её придётся вызывать “вручную”.
Знакомства с generic view мы начали в 4-м уроке, и уже тогда убедились, насколько это мощный и лаконичный инструмент Django. А из примера выше, мы также узнали, что TemplateView вообще может быть записано в одну строку прямо в конфигураторе shop/urls.py.
Два других представителя этой категории, а именно: ListView и DetailView, обладают такими же удивительными свойствами. В простейшем варианте вью можно указать только имена модели и шаблона. И этого будет вполне достаточно для того, чтобы html-шаблон получил всю необходимую информацию и сумел бы отобразить все строки таблицы Продуктов (ListView), или все поля одного единственного продукта, чей pk будет указан в конце url (DetailView).
(Справедливости ради стоит отметить, что даже имя шаблона указывать совсем необязательно - по умолчанию это имя уже содержится в настройках generic view. Дополнительную информацию можно получить здесь: Class-based views)
При переходе на страницу товаров по ссылке /shop/ пользователь ожидает увидеть весь список товаров. Поэтому меняем уже созданное тестовое вью TemplateView, которое было создано исключительно для проверки шаблона, на новое - ProductsListView:
from django.views.generic import ListView
from shop.models import Product
class ProductsListView(ListView):
model = Product
template_name = 'shop/shop.html'
Не забываем также внести изменения в конфигуратор shop/urls.py:
urlpatterns = [
path('', views.ProductsListView.as_view(), name='shop'),
path('cart_view/',
TemplateView.as_view(template_name='shop/cart.html'), name='cart_view'),
path('detail/<int:pk>/',
TemplateView.as_view(template_name='shop/shop-details.html'),
name='shop_detail'),
path('fill-database/', views.fill_database, name='fill_database'),
]
По умолчанию, все данные модели Product, будут автоматически переданы в шаблон в виде объекта object_list. Поэтому, создав в html-шаблоне цикл по object_list, мы получим доступ ко всем продуктам product, а значит сможем получить значения всех интересующих нас полей:
# one block
{% for product in object_list %}
<div class="col-12 col-lg-4 col-md-6 item">
<div class="card" style="width: 18rem;">
<form method="post" action="">
<img src="{{product.image_url}}" class="card-img-top" alt="...">
<div class="card-body">
<h5 class="card-title"><b>{{ product.name}}</b></h5>
<p class="card-text">
{{ product.description }}
</p>
</div>
<ul class="list-group list-group-flush">
<li class="list-group-item">Price: {{ product.price }}</li>
<li class="list-group-item">
{% csrf_token %}
<label class="form-label" for="id_quantity">Quantity:</label>
<input type="number" name="quantity" value="1" min="1"
required id="id_quantity"/>
</li>
</ul>
<div class="card-body">
<button class="learn-more-btn" type="submit">buy now</button>
<a class="contactus-bar-btn f_right" href="">detail</a>
<br><br>
</div>
</form>
</div>
</div>
{% endfor %}
В шаблоне shop/shop.html сейчас находятся 4 одинаковых тестовых блока. Заменив один из них на предложенный вариант, мы получим полных список всех блоков со всем значениями таблицы продукт.
Такое же лаконичное и изящное решение существует в Django и для отображения отдельно выбранного объекта. Только теперь наследуется не ListView, а DetailView:
class ProductsDetailView(DetailView):
model = Product
template_name = 'shop/shop-details.html'
И не забываем изменить имя вью в shop/urls.py:
urlpatterns = [
path('', views.ProductsListView.as_view(), name='shop'),
path('cart_view/',
TemplateView.as_view(template_name='shop/cart.html'), name='cart_view'),
path('detail/<int:pk>/', views.ProductsDetailView.as_view(),
name='shop_detail'),
path('fill-database/', views.fill_database, name='fill_database'),
]
Теперь, когда отображается реальный список продуктов, мы можем добавить ссылку на страницу детализации в цикл отображения продуктов product в шаблоне shop/shop.html:
<div class="card-body">
<button class="learn-more-btn" type="submit">buy now</button>
<a class="contactus-bar-btn f_right" href="{% url 'shop_detail' product.pk %}">detail</a>
<br><br>
</div>
Обратите внимание, как создаётся новая составная ссылка {% url 'shop_detail' product.pk %}: через пробел от имени url идёт номер продукта в БД - product.pk. Разумеется, этот результат тоже необходимо проверить.
Вот мы и подошли к одному из самых ответственных моментов - к наполнению корзины выбранным товаром. Разумеется, и эту логику также можно реализовать с помощью generic view. Более того, этот способ считается предпочтительным, поскольку, как известно, чем сложнее задача, тем более понятно и лаконично выглядит код generic view, по сравнению с кодом обычной функции.
Но, такой вариант может оказаться не очень понятным для тех, кто пока ещё на “вы” с объектно-ориентированным программированием. Поэтому, для решения этой задачи снова вернёмся к функциям и формам.
Начнём с формы. Заполнять нам предстоит таблицу OrderItem, которая по ForeignKey связана с таблицами (моделями) Product и Order. Поэтому, по сути, единственным неизвестным полем, которое нам предстоит ввести - это поле количества Quantity, а все остальные данные мы можем взять из других таблиц. Поэтому наша форма AddQuantityForm будет состоять только из одного поля:
from django import forms
from shop.models import OrderItem
class AddQuantityForm(forms.ModelForm):
class Meta:
model = OrderItem
fields = ['quantity']
Теперь вернёмся к вью, которое назовём add_item_to_cart. И здесь наша задача предельно упрощается - нам не нужно создавать GET-запрос. Ведь с этим отлично справляется ProductListView. Поэтому всё, что требуется от вью add_item_to_cart - это получить и обработать POST-запрос:
@login_required(login_url=reverse_lazy('login'))
def add_item_to_cart(request, pk):
if request.method == 'POST':
quantity_form = AddQuantityForm(request.POST)
if quantity_form.is_valid():
quantity = quantity_form.cleaned_data['quantity']
if quantity:
cart = Order.get_cart(request.user)
# product = Product.objects.get(pk=pk)
product = get_object_or_404(Product, pk=pk)
cart.orderitem_set.create(product=product,
quantity=quantity,
price=product.price)
cart.save()
return redirect('cart_view')
else:
pass
return redirect('shop')
Как видим, если форма прошла валидацию, то создаётся объект quantity. Помним также, что все объекты OrderItem существуют не сами по себе, а обязательно привязаны к какой-то корзине или заказу. Метод get_cart, который мы уже создали на предыдущих занятиях, способен обеспечить нас нужной корзиной - объектом cart. Объект product, чьё количество мы только что подтвердили, леко получается по запросу для pk=pk. Кстати, product можно сделать с помощью метода get(), но более надёжным считается вариант get_object_or_404(), который сможет обработать ошибку 404, если объекта с искомым pk не окажется в базе данных.
Таким образом, все поля, необходимые для создания нового объекта, мы уже получили. Поэтому теперь с помощью cart.orderitem_set.create() создаём новый объект модели OrderItem, а с помощью метода cart.save(), фиксируем связь этого объекта с корзиной заказа.
Последнее, что нам теперь остаётся, это добавить новый url в конфигуратор:
path('add-item-to-cart/<int:pk>', views.add_item_to_cart, name='add_item_to_cart'),
и затем добавить этот новый url в атрибут action тега формы на странице shop/shop.html:
<form method="post" action="{% url 'add_item_to_cart' product.pk %}">
Теперь всё готово к добавлению заказов в корзину. Правда, увидеть результат мы пока сможем лишь в админке.
И очень важный штрих, который едва не остался за кадром. Смотреть на каталог продукции должны иметь возможность все пользователи. А вот выбирать товар и добавлять его в корзину могут только зарегистрированные. Решить задачу защиты вью add_item_to_cart, от несанкционированного доступа неавторизованных пользователей, поможет декоратор @login_required. Как видим, этот декоратор автоматически перенаправит не залогиненного пользователя на страницу авторизации 'login'.
Конечно же, добавленные в корзину позиции хотелось бы видеть не только в админке. Тем более, что у нас уже всё для этого готово. Кроме вью. Им и займёмся.
По сути, для отображения элементов корзины необходимо получить данные двух моделей:
И затем передать эти данные в шаблон с помощью словаря context:
@login_required(login_url=reverse_lazy('login'))
def cart_view(request):
cart = Order.get_cart(request.user)
items = cart.orderitem_set.all()
context = {
'cart': cart,
'items': items,
}
return render(request, 'shop/cart.html', context)
Из объекта cart в шаблоне будут извлекаться только данные, относящиеся к самой корзине (в нашем случае - только сумма заказа). Данные по каждой позиции заказа item мы получим результате цикла по объекту items:
{% for item in items %}
<div class="row">
<div class="col-12 col-md-1 item">
{{ forloop.counter }}
</div>
<div class="col-12 col-md-4 item">
{{ item.product }}
</div>
<div class="col-12 col-md-2 item">
{{ item.quantity }}
</div>
<div class="col-12 col-md-2 item">
{{ item.price }}
</div>
<div class="col-12 col-md-2 item">
{{ item.amount }}
</div>
<div class="col-12 col-md-1 item">
</div>
</div>
{% endfor %}
Ошибаться может каждый. Поэтому пользователь должен иметь возможность удалять лишние позиции из корзины.
Как мы уже хорошо усвоили - все изменения базы данных должны проходить только через форму и метод POST. И удаление позиции в том числе.
Здесь стоит отметить, что для удаления элементов модели в Django имеется очень удобное generic view - DeleteView, для которого не нужно ни создавать отдельную форму в модуле shop/forms.py, ни специально описывать метод POST. Всё это уже создано в DeleteView по умолчанию:
@method_decorator(login_required, name='dispatch')
class CartDeleteItem(DeleteView):
model = OrderItem
template_name = 'shop/cart.html'
success_url = reverse_lazy('cart_view')
# Проверка доступа
def get_queryset(self):
qs = super().get_queryset()
qs.filter(order__user=self.request.user)
return qs
Единственное, что мы здесь добавили (точнее, изменили), так это метод get_queryset, которые фильтрует запрос данных модели OrderItem по пользователю user.
Никакого дополнительно вывода по методу GET нам тоже не нужно: как и в случае с добавлением позиции в корзину, мы воспользуется готовыми данными, которые нам любезно предоставляет cart_view.Всё, что нам остаётся - это добавить форму в КАЖДУЮ (!!!) позицию корзины (благо, все они всё равно выводятся в цикле) и новый url для вызова нового вью CartDeleteItem.
Изменения в шаблоне shop/cart.html:
<div class="col-12 col-md-1 item">
<form method="post" action="{% url 'cart_delete_item' item.pk %}">
{% csrf_token %}
<button type="submit" style="color: blue"><u>delete</u></button>
</form>
</div>
Добавление в shop/urls.pyl:
path('delete_item/<int:pk>', views.CartDeleteItem.as_view(), name='cart_delete_item'),
После того, как всё нужное будет успешно добавлено в корзину, а всё лишнее будет благополучно из неё удалено, всё, что нам остаётся - завершить процесс набора, изменить статус корзины с STATUS_CART на STATUS_WAITING_FOR_PAYMENT и тем самым перейти к оплате заказа.
Сам метод make_order у нас давно уже создан. Остаётся только добавить на лист корзины кнопку, по нажатию который будет запускаться этот метод. Задача для вью будет очень простая - найти нужную корзину и применить к ней метод make_order:@login_required(login_url=reverse_lazy('login'))
def make_order(request):
cart = Order.get_cart(request.user)
cart.make_order()
return redirect('shop')
После смены статуса произойдёт редирект на страницу shop/shop.html. После подключения онлайн оплаты, этот редирект можно будет заменить переходом на страницу агрегатора платежей. И ещё надо будет не забыть добавить в shop/urls.py новую ссылку:
path('make-order/', views.make_order, name='make_order'),
И добавить эту ссылку в кнопку на страницу корзины:
<a class="contactus-bar-btn f_right" href="{% url 'make_order' %}">
Process to Payment
</a>
Наш проект завершён. Конечно же многое осталось за кадром: подтверждение регистрации по имейл, логирование, подключение онлайн оплаты, деплоинг и т.д. и т.п.
Тем не менее, главные функционал интернет-магазина создан. А улучшению, как известно, никогда не бывает ни конца, ни края.
В любом случае, если возникнут вопросы, Вы знаете кому их можно задать: it4each.com@gmail.com.
Успеха в создании собственного интернет-магазина и до встречи на новых курсах!
Более подробно с со всеми деталями этого этапа вы сможете познакомиться из этого видео:
Внимание! Если у вас возникли проблемы с выполнением предыдущего этапа, то вы сможете зайти в соответствующий урок, скачать архив предыдущего этапа, инсталлировать его, и начать этот урок именно с того самого места, где закончился предыдущий!
Содержание курса:
Конечно же, самым удобным (и, кстати, более надёжным) способом будет взаимодействие нашего сайта с сайтом-поставщиком по API. Правда, для этого необходимо, чтобы
И хотя фреймворк Django позволяет организовать подобное взаимодействия собственными средствами, не прибегая к установке сторонних пакетов, тем не менее, лучше всего с решением этой задачи справится пакет Django REST ftamework (DRF), который обязательно будет рассмотрен в одном из наших ближайших курсов.
Однако, в нашем случае мы воспользуемся другим способом - считыванием необходимой информации непосредственно с HTML-страницы стороннего сайта. Это действие носит название скрейпинг (парсинг) сайта.
Для этой цели будут использованы две популярные Python библиотеки: beautifulsoup4 и requests. Установить их можно с помощью двух терминальных команд:
pip install beautifulsoup4
pip install requests
Обычно данные на странице товаров сгруппированы в блоки. Внутри блоков однотипные данные находятся под одинаковыми селекторами (см. рисунок):
Если скачать и “распарсить” HTML-страницу со списком товаров, мы можем получить структурированный список данных. Конкретно для нашего случая по каждому блоку данных нам нужно получить вот такой словарь:
{
'name': 'Труба профильная 40х20 2 мм 3м',
'image_url': 'https://my-website.com/30C39890-D527-427E-B573-504969456BF5.jpg',
'price': Decimal('493.00'),
'unit': 'за шт',
'code': '38140012'
}
План готов - приступаем к его реализации!
Очевидно, что скрипт, отвечающий за считывание информации с другого сайта следует разместить в отдельном модуле приложения shop: shop/scraping.py. За отправку запроса по адресу URL_SCRAPING, считывание данных и запись этих данных в таблицу Product базы данных проекта, будет отвечать функция scraping().
Прежде всего нам потребуется получить HTML-код страницы с данными по продукции для дальнейшей обработки. Выполнение этой задачи будет поручено модулю requests:
import requests
def scraping():
URL_SCRAPING = 'https://www.some-site.com'
resp = requests.get(URL_SCRAPING, timeout=10.0)
if resp.status_code != 200:
raise Exception('HTTP error access!')
data_list = []
html = resp.text
Есть смысл сразу же посмотреть, что получили. Для этого добавим код, который подсчитает число символов в объекте html и заодно выведет на печать сам этот объект:
html = resp.text
print(f'HTML text consists of {len(html)} symbols')
print(50 * '=')
print(html)
if __name__ == '__main__':
scraping()
Модуль shop/scraaping.py не требует настроек Django (во всяком случае пока), поэтому запустить его можно как обычный Python-скрипт:
HTML text consists of 435395 symbols
==================================================
<!DOCTYPE html>
<html lang="ru">
<head>
<link rel="shortcut icon" type="image/x-icon" href="/bitrix/templates/elektro_light/favicon_new.ico"/>
<meta name="robots" content="index, follow">
<meta name="keywords" content="Профильные трубы, уголки">
<meta name="description" content="Цены на профильные трубы, уголки от руб. Описание. Характеристики. Отзывы. Скидки на профильные трубы, уголки.">
<meta name="viewport" content="width=device-width, initial-scale=1.0 user-scalable=no"/>
<meta name="msapplication-TileColor" content="#ffffff">
Как видим результат действительно похож на HTML-страницу.
Первая часть задачи решена - доступ к данным сайта получен, и в тех 435 395 символах, что вывелась на экран, содержится вся необходимая нам информация. Всё, что нам теперь требуется - просто извлечь эту информацию и сохранить результат в БД.
Дальнейшую обработку удобнее всего будет вести с помощью модуля beautifulsoup4. Для этого нам потребуется сначала создать объект soup, представляющий собой вложенную структуру данных HTML-документа:
from bs4 import BeautifulSoup
soup = BeautifulSoup(html, 'html.parser')
Более подробную информацию о том, как начать работать с этим пакетом можно найти на странице руководства: BeautifulSoup #quick-start
Также очень полезной может быть дополнительная информация о CSS-селекторах пакета beautifulsoup4, которую можно прочитать здесь: BeautifulSoup #css-selectors
Далее на странице поставщика более всего нас будет интересовать блок товаров - вёрстка повторяющихся карточек товара, имеющих сходную структуру данных. Получить список однотипных элементов из объекта soup можно с помощью метода select(), где в качестве аргумента указан CSS-selector этого блока. В нашем случае это будет class=”catalog-item-card”:
blocks = soup.select('.catalog-item-card ')
В цикле мы можем получить доступ к каждому блоку, и заодно посмотреть, что находится внутри объекта block. Так станет выглядеть изменённый код:
html = resp.text
soup = BeautifulSoup(html, 'html.parser')
blocks = soup.select('.catalog-item-card ')
for block in blocks:
print(f'HTML text consists of {len(block.text)} symbols')
print(50 * '=')
print(block.text)
break
А вот так будет выглядеть распечатанный объект block.text:
HTML text consists of 382 symbols
==================================================
<div class="catalog-item-card" itemprop="itemListElement" itemscope="" itemtype="http://schema.org/Product">
<div class="catalog-item-info">
<div class="item-all-title">
<a class="item-title" href="/catalog/profilnye_truby_ugolki/truba_profilnaya_40kh20_2_mm_3m/" itemprop="url" title="Труба профильная 40х20 2 мм 3м">
<span itemprop="name">Труба профильная 40х20 2 мм 3м</span>
</a>
Как видим, число символов в блоке сократилось до 382-х. Что существенно упрощает нашу задачу.
Мы можем “разобрать” эти блоки на интересующие нас элементы с помощью метода soup.select_one(), который, в отличие от метода select(), выбирает не все элементы страницы, удовлетворяющее условию (аргументу метода), а только первый совпавший элемент. Важно также помнить, что текст, из полученного с помощью soup.select_one() объекта, можно извлечь с помощью метода text. Таким образом, применяя этот метод с определёнными аргументами, мы заполняем практически весь словарь данных, за исключением поля code:
soup = BeautifulSoup(html, 'html.parser')
blocks = soup.select('.catalog-item-card ')
for block in blocks:
"""{
'name': 'Труба профильная 40х20 2 мм 3м',
'image_url': 'https://my-website.com/30C39890-D527-427E-B573-504969456BF5.jpg',
'price': Decimal('493.00'),
'unit': 'за шт',
'code': '38140012'
}
"""
data = {}
name = block.select_one('.item-title[title]').get_text().strip()
data['name'] = name
image_url = URL_SCRAPING_DOMAIN + block.select_one('img')['src']
data['image_url'] = image_url
price_raw = block.select_one('.item-price ').text
# '\r\n \t\t\t\t\t\t\t\t\t\t\t\t\t\t493.00\t\t\t\t\t\t\t\t\t\t\t\t руб. '
price = re.findall(r'\S\d+\.\d+\S', price_raw)[0]
price = Decimal(price)
data['price'] = price # 493.00
unit = block.select_one('.unit ').text.strip()
# '\r\n \t\t\t\t\t\t\t\t\t\t\t\t\t\tза шт\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t'
data['unit'] = unit # 'за шт'
При детальном рассмотрении блока товаров block оказалось, что часть информации по товару находится на другой странице сайта поставщика - на странице детализации товара. Сама ссылка для перехода на эту страницу, находится в блоке block.
Поэтому придётся повторить здесь тот же самый алгоритм, который был использован нами несколько шагов тому назад для получения данных по блоку block:
# find and open detail page
url_detail = block.select_one('.item-title')
# <a class="item-title" href="/catalog/profilnye_truby_ugolki/truba_profilnaya_40kh20_2_mm_3m/" itemprop="url" title="Труба профильная 40х20 2 мм 3м">
url_detail = url_detail['href']
# '/catalog/profilnye_truby_ugolki/truba_profilnaya_40kh20_2_mm_3m/'
url_detail = URL_SCRAPING_DOMAIN + url_detail
html_detail = requests.get(url_detail).text
soup = BeautifulSoup(html_detail, 'html.parser')
code_block = soup.select_one('.catalog-detail-property')
code = code_block.select_one('b').text
data['code'] = code
data_list.append(data)
print(data)
Если мы всё сделать правильно, то в итоге получим список словарей с данными по каждому блоку.
Успех скрейпинга сайтов зависит от целого ряда параметров и обстоятельств. И большинство из них не зависит от нашего кода в Django, а именно:
Разумеется, избежать всех этих ошибок не в наших силах. Но в наших силах перехватить эти ошибки, чтобы понять и, по-возможности, минимизировать или обойти причину их возникновения.
Удобнее всего это сделать с помощью класса Исключений (Exceptions). Прежде всего создаём класс исключений скрейпинга ScrapingError. И далее Наследует от него 3 новый пользовательских класса:
class ScrapingError(Exception):
pass
class ScrapingTimeoutError(ScrapingError):
pass
class ScrapingHTTPError(ScrapingError):
pass
class ScrapingOtherError(ScrapingError):
pass
И далее вносим соответствующие изменения в код:
try:
resp = requests.get(URL_SCRAPING, timeout=10.0)
except requests.exceptions.Timeout:
raise ScrapingTimeoutError("request timed out")
except Exception as e:
raise ScrapingOtherError(f'{e}')
if resp.status_code != 200:
raise ScrapingHTTPError(f"HTTP {resp.status_code}: {resp.text}")
Итак, данные по каждому блоку получены и преобразованы в удобный формат (список словарей). Теперь остаётся добавить эти данные в базу данных. Иными словами, заменить строчку кода print(data) на код, заполняющий таблицу Product:
for item in data_list:
if not Product.objects.filter(code=item['code']).exists():
Product.objects.create(
name=item['name'],
code=item['code'],
price=item['price'],
unit=item['unit'],
image_url=item['image_url'],
)
return data_list
Как видно, продукт добавляется только в том случае, если его ещё нет в БД. Поиск ведётся по номеру кода продукта (значение поля code ).
Несмотря на то, что в самой функции scraping.py данные уже записываются в БД, мы всё равно возвращаем список data_list. Просто на всякий случай).
Однако, если сейчас мы попробуем воспроизвести этот скрипт, то получим ошибку:
"/home/su/Projects/django-apps/Projects/drop-ship-store/venv/lib/python3.8/site-packages/django/conf/__init__.py", line 67, in _setup
raise ImproperlyConfigured(
django.core.exceptions.ImproperlyConfigured: Requested setting INSTALLED_APPS, but settings are not configured. You must either define the environment variable DJANGO_SETTINGS_MODULE or call settings.configure() before accessing settings.
Process finished with exit code 1
Всё дело в том, что теперь скрипт обращается к БД, а значит, требуется получить установки Django. Можно запустить этот код для проверки в management/commands (более подробно об этом можно найти здесь: https://docs.djangoproject.com/en/4.0/howto/custom-management-commands/) Но мы поступим иначе: сразу же добавим страницу запуска и проверим работу функции scraping() уже там.
Алгоритм добавления новой страницы остаётся прежним:
Если после успешного выполнения всех этих пунктов мы зайдём в админку, то увидим что, после запуска скрипта, таблица Product наполнилась новыми значениями:
Теперь всё готово для последнего шага: добавления непосредственно страниц интернет магазина и кода, который будет ими управлять. Но этим мы займёмся уже на следующем седьмом и последнем занятии.
Более подробно с со всеми деталями этого этапа вы сможете познакомиться из этого видео:
Внимание! Если у вас возникли проблемы с выполнением предыдущего этапа, то вы сможете зайти в соответствующий урок, скачать архив предыдущего этапа, инсталлировать его, и начать этот урок именно с того самого места, где закончился предыдущий!
Содержание курса:
Большинство программистов не любит писать тесты.
Оно и понятно - мало кто любит делать двойную работу: сначала написать и отладить код, а потом ещё столько же времени (если не больше!) затратить на написание тестов.
Хорошо, если проект будет поддерживаться и дальше. Тогда время, затраченное на написание тестов, гарантированно окупится: убедиться в том, что создавая новое мы не сломали старое, можно будет за секунды.
Однако, если проект будет закрыт, то к потерям времени на написание основного кода добавятся ещё и затраты на тесты.
Так что написание тестов чем-то похоже на инвестирование - дело, в принципе, хорошее, но рискованное. Поэтому писать тесты или не писать - каждый выбирает для себя сам в зависимости от каждой конкретной ситуации.
Тем не менее, есть подход в программировании, который позволяет если не выиграть, то, как минимум, почти не проиграть при написании тестов. И называется это подход "Test Driven Development" (TDD) ("Разработка через тестирование"). Идея очень проста: мы СНАЧАЛА пишем тесты, и только потом пишем сам код, который должен пройти эти тесты.
И в чём же здесь плюс? А плюс в том, что в этом случае полностью исключается затраты времени на проверку кода. Ведь даже самый первый прогон свеже-написанного кода будет делаться уже "не руками", а тестами!
И дополнительный бонус этого подхода - сама проверка становится более системной, а значит более надёжной. Поскольку, что и как тестировать - обдумывается заранее, а не делается экспромтом. Что резко уменьшает шансы упустить из виду что-то важное.
Собственно, именно поэтому в Django достаточно хорошо разработан инструментарий для тестирования кода. И именно поэтому уже в первом учебном примере на сайте этого фреймворка целый урок из восьми отводится на рассмотрение примера реализации этого принципа: "Разработка через тестирование" или "Test Driven Development" (TDD).
В нашем учебном мини-курсе мы рассмотрим подход разработки через тестирование при создании базы данных, поскольку основная вычислительная нагрузка или основная бизнес-логика будет находится именно в модуле shop/models.py, отвечающем за взаимодействие между таблицами базы данных.
Итак, наши задачи на этом этапе:
Для упрощения задачи (всё-таки, это мини-курс!) оговорим три допущения:
Таким образом, структура данных будет состоять из 4-х таблиц:
Ещё нам потребуется таблица Клиентов User, но на наше счастье она уже присутствует в Django по умолчанию, хотя и в самой простой базовой версии. Однако, на этапе учебного проекта этого будет более, чем достаточно.
Всем вышеперечисленным требованиям вполне удовлетворяет эта блок-схема:
Поэтому в модуле shop/models.py создаём следующую структуру:
class Product(models.Model):
name = models.CharField(max_length=255, verbose_name='product_name')
code = models.CharField(max_length=255, verbose_name='product_code')
price = models.DecimalField(max_digits=20, decimal_places=2)
unit = models.CharField(max_length=255, blank=True, null=True)
image_url = models.URLField(blank=True, null=True)
note = models.TextField(blank=True, null=True)
class Payment(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
amount = models.DecimalField(max_digits=20, decimal_places=2, blank=True, null=True)
time = models.DateTimeField(auto_now_add=True)
comment = models.TextField(blank=True, null=True)
class Order(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
# items = models.ManyToManyField(OrderItem, related_name='orders')
status = models.CharField(max_length=32, choices=STATUS_CHOICES, default=STATUS_CART)
amount = models.DecimalField(max_digits=20, decimal_places=2, blank=True, null=True)
creation_time = models.DateTimeField(auto_now_add=True)
payment = models.ForeignKey(Payment, on_delete=models.PROTECT, blank=True, null=True)
comment = models.TextField(blank=True, null=True)
class OrderItem(models.Model):
order = models.ForeignKey(Order, on_delete=models.CASCADE)
product = models.ForeignKey(Product, on_delete=models.PROTECT)
quantity = models.PositiveIntegerField(default=1)
price = models.DecimalField(max_digits=20, decimal_places=2)
discount = models.DecimalField(max_digits=20, decimal_places=2, default=0)
Чтобы объекты этих классов понятно отображались при отладке и в админке, полезно будет добавить в каждый класс методы __str__, а также упорядочить все записи в таблицах по порядку, например:
class Meta:
ordering = ['pk']
def __str__(self):
return f'{self.user} --- {self.time.ctime()} --- {self.amount}'
Завершающим шагом создания БД будет создание миграция и их применение:
python manage.py makemigrations
python manage.py migrate
И последний штрих - добавить управление и отображение БД в админку:
from django.contrib import admin
from shop.models import Product, Payment, OrderItem, Order
admin.site.register(Product)
admin.site.register(Payment)
admin.site.register(OrderItem)
admin.site.register(Order)
Как видим, список проверок достаточно внушительный. Даже однократное ручное выполнение всех перечисленных тестов - “та ещё работёнка”, которая может занять минут 15, не меньше.
Но мы-то хорошо знаем из собственного опыта, что редкий код начинает работать без ошибок с первого раза. Следовательно, ручное тестирование заняло бы часы нашего времени. А каждый новый рефакторинг кода - это снова часы на проверку. Не говоря уже о том, что при ручном тестирование можно что-то забыть проверить, или забыть удалить тестовые значения из БД.
В общем, как мы сами убедимся далее, написание тестов займёт меньше времени уже с первого раза, а значит окупит свои затраты уже на первом тестовом прогоне!
При написании тестов необходимо помнить следующее:
fixtures = [
"shop/fixtures/data.json"
]
Пример содержимого файла тестов shop/tests.py представлен ниже:
from django.test import TestCase,
from shop.models import *
class TestDataBase(TestCase):
fixtures = [
"shop/fixtures/data.json"
]
def setUp(self):
self.user = User.objects.get(username='root')
def test_user_exists(self):
users = User.objects.all()
users_number = users.count()
user = users.first()
self.assertEqual(users_number, 1)
self.assertEqual(user.username, 'root')
self.assertTrue(user.is_superuser)
def test_user_check_password(self):
self.assertTrue(self.user.check_password('123'))
Пример фрагмента кода, используемого в лекции, представлен ниже:
def change_status_after_payment(payment: Payment):
"""
Calling the method after creating a payment and before saving it.
First need to find total amount from all payments (previous and current) for the user.
If total amount >= the order amount, we change status and create negative payment.
"""
user = payment.user
while True:
order = Order.objects.filter(status=Order.STATUS_WAITING_FOR_PAYMENT, user=user) \
.order_by('creation_time') \
.first()
if not order:
break
total_payments_amount = get_balance(payment.user)
if order.amount > total_payments_amount:
break
order.payment = payment
order.status = Order.STATUS_PAID
order.save()
Payment.objects.create(user=user, amount=-order.amount)
@receiver(post_save, sender=Payment)
def on_payment_save(sender, instance, **kwargs):
if instance.amount > 0:
# pay_for_waiting_orders(instance)
change_status_after_payment(instance)
@receiver(post_save, sender=Order)
def on_order_save(sender, instance, **kwargs):
if Order.objects.filter(status='2_waiting_for_payment'):
pay_for_waiting_orders(instance)
Более подробно с со всеми деталями этого этапа вы сможете познакомиться из этого видео:
Внимание! Если у вас возникли проблемы с выполнением предыдущего этапа, то вы сможете зайти в соответствующий урок, скачать архив предыдущего этапа, инсталлировать его, и начать этот урок именно с того самого места, где закончился предыдущий!
Содержание курса:
Как уже неоднократно упоминалось, формат этого курса предполагает в первую очередь практику и ещё раз практику. И всё же, без определения понятий сессия, cookies, GET- и POST-запросы будет очень сложно понять суть того, что мы будет делать дальше. Поэтому, несколько слов об этом, всё же, стоит сказать.
Как сайте “узнает”, что на него заходят “свои”, а не “чужие”?
Давайте откроем в браузере вкладку Storage → Cookies и попробуем зарегистрироваться на сайте. Мы увидим, что в момент регистрации появится новый ключ session_id, который будет существовать ровно до тех пор, пока мы авторизованы на этом сайте. Если мы откроем базу данных этого сайта, то в таблице в таблице django_session увидим тот же самый сессионный ключ.
Однако, если мы разлогинимся, то этот session_id исчезнет и из базы, и из cookies браузера. Дело в том, что после авторизации пользователя на сайте, сервер создаёт в своей БД особую цифровую метку, копию который он отправляет браузеру пользователя, и браузер сохраняет эту цифровую метку в своей памяти. Это и есть ключ сессии, который сохраняется в памяти браузера. Эти данные, сохранённые в памяти браузера и называются cookies.
(На всякий случай: браузер для SQLite, упомянутый в видео, можно установить с помощью этих команд:
$ sudo add-apt-repository -y ppa:linuxgndu/sqlitebrowser
$ sudo apt-get update
$ sudo apt-get install sqlitebrowser )
До тех пор, пока пользователь залогинен на сайте, session_id в cookies его браузера совпадает с session_id, сохраненными в БД. Что даёт возможность пользователю посещать любые страницы сайте, к которым у него есть доступ.
Для этого браузер при каждом новом запросе на сервер отправляет туда session_id, и пока этот код совпадает с кодом в БД сервера, пользователь остаётся авторизованным.
Если же пользователь разлогинится, то сервер удалит его session_id из своей базы данных. И на те страницы сайта, где требуется авторизованный вход, этот пользователь войти уже на сможет.
Если же пользователь залогинится снова, то он опять сможет посещать любые страницы сайта. Но это уже будет совсем другая сессия с другим session_id.
Все таблицы в базе данных (БД) Django описываются с помощью моделей. Это специальные Python классы, наследуемые от класса Model.
Одна модель описывает одну таблицу БД. А каждая строка этой модели описывает одно поле таблицы.
Более подробно мы разберём примеры моделей на следующем 5-м уроке. Ну, а пока просто примем к сведению, что модель пользователя User, имеющая определённый набор базовых полей и методов, уже существуют по умолчанию. Она всегда создаётся в Django во время первой миграции. В частности, там уже есть поля username, email, password, is_staff (является ли пользователь сотрудником) и is_superuser.
Новые незнакомые аббревиатуры обычно внушают страх. Чтобы избавиться от, как минимум, половины этого страха, сразу же отметим, что все запросы, о которых мы говорили до сих пор - это GET запросы. Правда, ведь, совсем не страшно?!! Тогда идём дальше!
Итак, при обращению к серверу возможны разные типы запросов. Вообще, HTTP протокол имеет их достаточно много, но в данном курсе мы будем использовать только два:
Для передачи GET-запроса достаточно указать обычный адрес url. А вот передача POST запроса ОБЯЗАТЕЛЬНО предполагает наличие формы и csrf-токена.
Форма в HTML - это специальная конструкция, которая заключена в тег <form></form>. Внутри этого могут также содержаться поля ввода с одиночным тегом <input>, которые обычно идут в паре с тегом <label></label>, который поясняет, что именно надо вводить в поле input.
Форма также содержит кнопку (поле ввода) с type="submit", нажатие на которую как раз и является командой отправки данных на сервер.
Примерно ток выглядит форма в коде HTML:
<form method="post" action="/your-url/">
<label for="username">User name: </label>
<input id="username" type="text" name="username" value="{{ username }}">
<input type="submit" value="OK">
</form>
В предыдущем уроке мы уже научились передавать управление из конфигуратора main/urls.py во view. Однако, хорошей практикой является и другой вариант: промежуточная передача url из главного конфигуратора в конфигуратор другого приложения.
Для этого в main/urls.py мы добавим строку, благодаря который все запросы авторизации, начинающиеся с ‘auth/’ будут передаваться на обработку в другой конфигуратор - authentication/urls.py:
from django.contrib import admin
from django.urls import path, include
from company import views as views_company
urlpatterns = [
path('', views_company.home_page, name='index'),
path('about/', admin.site.urls),
path('admin/', admin.site.urls),
path('auth/', include('authentication.urls')),
]
Следовательно, в authentication/urls.py появятся запросы, соответствующие авторизации, регистрации и разлогиниванию:
from django.urls import path
from authentication import views
urlpatterns = [
path('login/', views.login_user, name='login'),
path('register/', views.register, name='register'),
path('logout/', views.logout_user, name='logout'),
]
Теперь, для начала сделаем простые view, которые будут запускаться по этим трём запросам в приложении authentication:
def login_user(request):
return render(request, ‘auth/login.html’)
def register(request):
return render(request, ‘auth/register.html’)
def logout_user(request):
pass
C разлогиниванием пока ничего не понятно - страницы под него не предусмотрено, поэтому вместо кода просто напишем pass.
Ну а, вообщето, шаблон здесь и не нужен. А нужна специальная функция разлогинивания logout(request)
Тем не менее, если мы ограничимся во view только этой функцией, то получим ошибку: The view authentication.views.logout_user didn't return an HttpResponse object. It returned None instead.
Поэтому в конце view нам придётся вернуть переход на какую-либо страницу. Например на главную. Сделать это можно с помощью функции redirect('index'), где 'index' - имя адреса в authentication/urls.
Очевидно, что теперь конструкция для всех view в приложении авторизации будет не такой простой, как была для index view. Поскольку, теперь у нас появилась новая задача: прочитать данные с HTML-страницы, передать их на сервер и далее в таблицу User нашей базы данных . И, как уже говорилось выше, всё, что передёются в базу данных, должно передаваться только с помощью формы и POST-запроса.
Для создания форм в Django предусмотрен специальный модуль forms.py, в котором можно предварительно создать объект "форма ввода" и подробно описать его свойства и характеристики. Форма обычно связана с конкретной таблицей в базе данных. Поэтому, создавая форму следует:
Поскольку мы используем уже готовую Django’вскую таблицу User с довольно-таки объёмным списком стандартных полей, то в нашей форме обязательно следует указать какие именно поля будут использованы:
from django import forms
from django.contrib.auth.models import User
class LoginForm(forms.Form):
username = forms.CharField()
password = forms.CharField(widget=forms.PasswordInput())
Последняя строка говорит о том, что при вводе пароля будет использоваться специальный виджет, который позволяет скрывать символ при заполнении поля.
В следующем уроке мы увидим, что создание формы внешне очень похоже на создание таблицы базы данных.
Таким образом, в наше вью login_user необходимо будет добавить следующие опции:
Передать любые данные на страницу можно с помощью дополнительно созданного словаря context. В нашем варианте по ключу login_form мы добавим экземпляр класса только что созданной формы LoginForm():
def login_user(request):
context = {'login_form': LoginForm()}
return render(request, 'auth/login.html', context)
Для передачи данных на html-страницу этого вполне достаточно, но для получения данных со страницы потребуется добавить обработку данных по POST-запросу.
def login_user(request):
context = {'login_form': LoginForm()}
if request.method == 'POST':
login_form = LoginForm(request.POST)
if login_form.is_valid():
username = login_form.cleaned_data['username']
password = login_form.cleaned_data['password']
user = authenticate(username=username, password=password)
if user:
login(request, user)
return redirect('index')
return render(request, 'auth/login.html', context)
Одновременно с этим на html-странице необходимо будет добавить:
То есть фрагмент формы в HTML-коде будет выглядеть примерно так:
<form method="post" action="{% url 'login' %}">
{% csrf_token %}
<label class="form-label" for="id_username">Login</label>
<input type="text" class="form-input" id="id_username"
name="username" placeholder="Login" required>
<label class="form-label" for="id_password">Password</label>
<input type="password" class="form-input" id="id_password"
name="password" placeholder="Password" required>
<button type="submit" class="request-quote-btn">Log In</button>
</form>
Передача данных от пользователя - очень чувствительная операция, с помощью которой потенциальный злоумышленник может попытаться проникнуть на сервер. Поэтому для дополнительной безопасности, помимо уже существующего ключа сессии, на страницу передаётся ещё один секретный ключ csrf_token. Он валиден только на одну передачу данных из конкретной формы, и исключительно только для передачи данных именно этой формы. Если что-то пойдёт не так и потребуется новый ввод данных в форму, то при новом запросе сервер сформирует новый ключ csrf_token.
Заполнение формы может быть с ошибками - это совершенно нормально. Не нормально, если сервер “молча” не примет эти данные и не “объяснит” пользователю почему.
Есть, как минимум, два способа.
1.) Самое простое решение - просто сообщить пользователю, что введённые данные не верны. Для этого в словарь context просто добавляется новое поле attention. Кроме того, правильно будет вернуть форму именно с теми данными, которые не прошли.
if user:
login(request, user)
return redirect('index')
context = {
'login_form': LoginForm(request.POST),
'attention': f'The user with name {username} is not registered in the system!'}
2.) Другой, более изящный и аккуратный способ: сообщить пользователю какие именно ошибки возникли в процессе валидации. И здесь потребуется изменения непосредственно в самой форме:
def clean(self):
cleaned_data = super().clean()
username = cleaned_data.get('username')
password = cleaned_data.get('password')
try:
self.user = User.objects.get(username=username)
except User.DoesNotExist:
raise forms.ValidationError(f'User with username: [{username}] does not exist!')
if not self.user.check_password(password):
raise forms.ValidationError('Could not log in using these email and password')
return cleaned_data
Теперь эти ошибки будут добавлены к экземпляру класса формы LoginForm(). Правда, чтобы их прочитать на странице авторизации, также потребуются незначительные изменения HTML кода:
{% for error in login_form.non_field_errors %}
<div class="alert alert-danger">{{ error }}</div>
{% endfor %}
На этом миссию создания страницы авторизации можно считать выполненной!
Помимо уже хорошо известных нам вью-функций, есть также вью-классы, наследуемые от generic views. Более того, последним отдаётся предпочтение при решении сложных задач, поскольку generic views выглядят более компактно и лучше читаются.
Наше знакомство с темой generic views мы начнём пока только с одного варианта - класса TemplateView. (Далее в этом курсе мы рассмотрим ещё несколько видов generic views).
В качестве примера перепишем наше register вью, выполненное на основе функции, в RegisterView с использованием класса TemplateView:
from django.views.generic import TemplateView
class RegisterView(TemplateView):
template_name = 'auth/register.html'
Для обработки GET и POST запросов у этого класса есть методы get и post:
def get(self, request):
user_form = RegisterForm()
context = {'user_form': user_form}
return render(request, 'auth/register.html', context)
def post(self, request):
user_form = RegisterForm(request.POST)
if user_form.is_valid():
user = user_form.save()
user.set_password(user.password)
user.save()
login(request, user)
return redirect('index')
context = {'user_form': user_form}
return render(request, 'auth/register.html', context)
На способ создания записи нового пользователя в таблице User стоит обратить особое внимание, так как этот способ мы будем применять и далее:
О форме RegisterForm, которая используется здесь в методе post, следует сказать отдельно.
Предыдущая форма LoginForm была создана на основе класса Form - довольно-таки простого варианта, внешне очень похожего на класс моделей Model.
Формы, созданные с помощью класса ModelForm, являются более “продвинутым” вариантом. Здесь нам уже нет надобности прописывать каждое поле формы, - достаточно просто указать имя модели, которая будет использоваться в этой форме, и список необходимых полей. После этого, формат полей из привязанной к форме модели автоматически перенесётся в саму форму.
Пример новой формы:
class RegisterForm(forms.ModelForm):
class Meta:
model = User
fields = ('username', 'email', 'password')
В предыдущем примере с формой авторизации LoginForm мы предприняли попытку встроить готовую форму в HTML-страницу с помощью тега {{ login_form }}. Однако, быстро отказались от этой затеи - уж больно сильно отличались стили стандартной Django-формы и стили вёрстки нашей страницы login.html.
Но решение есть. Надо только выводить поля не “за один раз”, а делать это постепенно, в цикле. Более того, вэтом случае, Django позволяет “разбить” отдельный вывод для каждого поля формы на три составляющих:
Пример HTML-кода, реализующие описанное выше:
{% for field in user_form %}
<div class="row">
<div class="col-12 col-lg-4 col-md-4 offset-md-4">
<div class="quote-form-wrapper">
<span style="color: red">{{ field.errors }}</span>
{{ field.label_tag}}
{{ field }}
</div>
</div>
</div>
{% endfor %}
Более подробно с со всеми деталями этого этапа вы сможете познакомиться из этого видео:
Внимание! Если у вас возникли проблемы с выполнением предыдущего этапа, то вы сможете зайти в соответствующий урок, скачать архив предыдущего этапа, инсталлировать его, и начать этот урок именно с того самого места, где закончился предыдущий!
Содержание курса:
Итак, наш нестандартный чек-лист (в виде доработанного HTML-шаблона) создан и теперь самое время приступить к "зачёркиванию" его пунктов. Начнём с главной страницы.
Прежде всего необходимо убедиться, что в корень проекта были добавлены каталоги tamplates для html-файлов и static для файлов статики (css-, js-и image-файлов). И что в файл настроек проекта settings.py были добавлены пути этих каталогов относительно базовой директории.
Собственно, убедиться в этом совсем несложно: актуальная схема-дерево нашего проекта представлена на слайде ниже:
├── authentication
│ ├── admin.py
│ ├── apps.py
│ ├── __init__.py
│ ├── migrations
│ │ └── __init__.py
│ ├── models.py
│ ├── tests.py
│ └── views.py
├── db.sqlite3
├── main
│ ├── asgi.py
│ ├── __init__.py
│ ├── settings.py
│ ├── urls.py
│ └── wsgi.py
├── manage.py
├── requirements.txt
├── shop
│ ├── admin.py
│ ├── apps.py
│ ├── __init__.py
│ ├── migrations
│ │ └── __init__.py
│ ├── models.py
│ ├── tests.py
│ └── views.py
├── static
├── templates
└── venv
Теперь копируем статику: копируем в папку проекта static всё содержимое папки asserts HTML-шаблона. Далее копируем главную страницу, пока так, как она есть (т.е. пока без выделения общей части в базовую страницу base-page.html). Просто скопируем файл шаблона index.html в папку templates нашего проекта.
Далее, нам нужно помнить, что для отображения страницы в Django-проекта необходимо, как минимум, 2 действия:
Сейчас в нашем проекте помимо главной папки main присутствуют ещё две папки приложений: папка shop и папка authentication. Во всех этих трёх папках уже есть, или может быть добавлена пара файлов urls.py - views.py. Но только одна из них: пара из главной папки main может обеспечить url, в котором не будет дополнительной добавки фрагментов адреса url в виде /auth/ или /shop/. Иными словами ссылки (urls) из папки main будут вести на "чистый" адрес имени домена.
Разумеется, совершенно логично, когда главная страница отображается по адресу имени домена. Так что можно сказать, что с местом размещения пары urls.py - views.py мы определились, осталось только добавить в папку main пока что недостающий там файл views.py. А далее сделать в этих файлах соответствующие записи:
main/urls.py
from django.contrib import admin
from django.urls import path
from shop import views
urlpatterns = [
path('', views.index, name='index'),
path('admin/', admin.site.urls),
]
Далее, создадим простейшее view, в задачи которого будет входить только рендеринг (создание) указанного шаблоне. Для его создания можно использовать готовый файл view одного из наших приложений, либо создать новый файл views.py в папке main.
main/views.py
from django.shortcuts import render
def index(request):
return render(request, 'index.html')
Пробуем запустить, и видим, что главная страница отобразилаcь, но стили на ней полностью отсутствуют. Оно и понятно: и ссылки на стили теперь должны быть прописаны совершенно на другие пути (раньше файлы статики были в папке asserts, а теперь она находится в папке static), и сам способ написания этих ссылок тоже должен стать совершенно другим.
<link rel="stylesheet" href="../construct/assets/css/style.css">
а вот так:
<link rel="stylesheet" href="{% static '/css/style.css' %}">
Обновляем страницу, и видим, что картина изменилась, но кроме надписи Loading... ничего не появляется. Значит надо обновить все ссылки на файлы статики главной страницы. И особенно, на файлы js в конце страницы...
Ну вот, теперь что-то видно. Определённые стили появились, хотя далеко не всё. Чтобы появились все, надо последовательно "пройтись" по всем ссылкам и аккуратно и внимательно их изменить. Как видим, без минимального знания HTML на бекенде не обойтись.
Ну, и чтобы, как говорится, два раза не вставать, сразу же оптимизируем для всех страниц нашу работу по приведению статики в соответствии с новыми требованиями.
Прежде всего необходимо обратить внимание на тот факт, что все страницы нашего HTML-шаблона имеют одну общую часть, в которую входят header (включая главное меню) и footer. Эти элементы присутствуют абсолютно на всех страницах. Поэтому нет никакого смысла сначала дублировать вставку одного и того же кода, а потом ещё и мучиться с изменением ссылок на файлы статики. Будет куда разумнее, если мы выделим эту общую часть в отдельную базовую страницу base-page.html, и оставим на этой базовой странице места вставок для уникального контента из нужных нам страницы проекта.
Теперь каждая HTML-страница, кроме базовой, будет иметь точно такую же структура, как и страница index.html:
{% extends 'base-page.html' %}
{% load static %}
{% block title %}
< Page Name >
{% endblock title %}
{% block container %}
< Current Page Content >
{% endblock container %}
То есть теперь при загрузке страницы index.html сначала идёт загрузка базовой страницы base-page.html (extends 'base-page.html'), затем загружается статика для этой страницы (load static), и далее в указанные места вставок добавляются текущие значение блоков < Page Name > и < Current Page Content >.
Аналогично будут оформлены и все прочие страницы. Например, для следующей страницы about-us.html нужно проделать те же самые операции:
main/urls.py
from django.contrib import admin
from django.urls import path
from shop import views
urlpatterns = [
path('', views.index, name='index'),
path('about/', views.about, name='about'),
path('admin/', admin.site.urls),
]
Далее, создадим простейшее view, в задачи которого будет входить только рендеринг (создание, построение) указанного шаблона. Для его создания можно использовать готовый файл view одного из наших приложений, либо создать новый файл views.py в папке main.
main/views.py
from django.shortcuts import render
def about(request):
return render(request, 'about-us.html')
И последний третий шаг: добавляем в папку templates страницу about-us.html. Изменённый HTML-код этой страницы, по сути, теперь будете состоять только из того кода, что раньше находился между header'ом и footer'ом. А структура страницы about-us.html ничем не будет отличаться от страницы index.html.
index.html
{% extends 'base-page.html' %}
{% load static %}
{% block title %}
< Page Name >
{% endblock title %}
{% block container %}
< Current Page Content >
{% endblock container %}
Таким образом, после создания базовой страницы, мы можем в шаблонах всех остальных страниц сайта размешать только тот оригинальный HTML-код, которого нет в базовом шаблоне. Разумеется, не забывая при этом давать ссылку на базовый шаблон. И всё! Это невероятно уменьшает дублирование кода и позволяет легко вносить изменения во все страницы сайта сразу изменяя, всего лишь, код на базовой странице.
Более подробно с со всеми деталями этого этапа вы сможете познакомиться из этого видео:
Внимание! Если у вас возникли проблемы с выполнением предыдущего этапа, то вы сможете зайти в соответствующий урок, скачать архив предыдущего этапа, инсталлировать его, и начать этот урок именно с того самого места, где закончился предыдущий!
Содержание курса:
На прошлом занятии мы подготовили среду разработки, создали в ней новый проект, добавили в этот проект необходимые настройки и даже успели создать два новых приложения, которые будут отвечать за регистрацию/авторизацию и за работу интернет-магазина. Иными словами, мы выполнили начальные типовые операции, которые соответствует подготовительному этапу 99,9(9)% подобных проектов. И теперь самое время составить подробный план наших дальнейших действий.
Планирование проектов, на самом деле, задача непростая. Для этих целей создана даже специальная научная дисциплина "Управление проектами" (Project management). Разумеется, формат нашего мини-курса не позволит нам слишком уж углубляться в эту. И, тем не менее, пару слов об управлении проектами сказать всё-таки нужно.
Думаю, ни для кого не открою Америки, если скажу, что выполнить проект можно и без всякого плана. Просто получится дольше и дороже. (Если вообще получится!)
Поэтому, отличительная особенность хорошего плана в том, что в каждом своём пункте он даёт ответы на 4 главных вопроса:
Если по всем пунктам плана есть ответы на эти вопросы - нас можно поздравить: у нас действительно есть план! Ну, а если - нет, значит у нас не план, а всего лишь мечта!
Но как быть, если мы собираемся сделать то, что ещё никто и никогда не делал? И если ни на один из этих четырёх вопросов нет ни одного ответа?
Ответ очень простой: нужно выделить это неизвестное направление, эту “Terra incognita” в отдельный план. План исследовательской работы.
Но как тут быть с последним пунктом, со сроками? Ведь у исследовательской работы крайне сложно определить сроки?
И эта проблема тоже решаема: там, где нельзя определить сроки, всегда можно установить дедлайн, который не даст этой исследовательской работе уйти в бесконечность.
Ну, а дальше всё просто: если мы смогли ответить на все неизвестные вопросы ДО наступления дедлайна, значит переходим ко второму этапу: оформляем результаты исследований в обычный план и начинаем работу. Ну, а если не уложились, значит, похоже, что мы взялись не за своё дело. Поэтому немедленно прекращаем эту затею и начинаем работать над решением другой задачи.
По счастью, задача, стоящая перед нами в этом мини-курсе, самая что ни есть обычная и тривиальная. Поэтому здесь надо просто аккуратно переписать все этапы нашего плана в чек-лист, и, по мере выполнения этих этапов, постепенно их вычёркивать, двигаясь от строчки к строчке.
Однако, в нашем мини-курсе мы пойдём ещё дальше, и в качестве такого "чек-листа" возьмём готовый HTML-шаблон, где каждая HTML-страница будет соответствовать этапу нашего плана. Порядок работы будет такой: перенесли страницу из шаблона в проект - считай зачеркнули строчку в чек-листе! И так до самого конца шаблона, пока не "повычёркиваем" из шаблона все HTML-страницы! Вероятно тому, кто ещё так не делал, эта идея покажется совершенно абсурдной. Но не спешите делать выводы!Вариантов ответа на этот вопрос, как минимум, три:
Все эти варианты имеют свои плюсы и минусы:
Итак, выбранный шаблон загружен с сайта Template Monster и распакован в отдельную папку. Если мы откроем в браузере файл index.html, то может показаться, что перед нами уже готовый проект: все (или почти все) кнопки и ссылки работают и ведут на нужные страницы. С одной, правда, существенной оговоркой: все эти красивые страницы наполнены не совсем той информацией, которая нам нужна. Следовательно, именно сейчас нам предстоит определить, какие именно страницы мы оставим, какие удалим, а какие, наоборот, добавим.
Давайте теперь определимся с набором страниц нашего будущего шаблона. Для нашего проекта потребуются следующие страницы:
Очевидно, что все эти страницы (кроме страницы детализации) должны иметь ссылки в главном меню. И желательно также чтобы меню регистрации/авторизации было бы отдельным от главного.
База данных будет максимально простой (всё-таки курс у нас мини, а не макси).
Таблица товаров будет “плоской”, т.е. без категорий/подкатегорий. Мера измерения товара будет “штуки”. Выбранный товар с установленным количеством автоматически попадает в корзину. Если корзины для этого пользователя ещё нет, она создаётся, если уже есть - дополняется.
Если товары из корзины отправлены на оплату, то корзина обнуляется и появляется Заказа, со статусом “ожидающий оплаты”.
Для учёта оплат должна быть создана таблица платежей. Если сумма зачисленной оплаты больше или равна сумме заказа, то Заказ меняет статус на “оплаченный”, а в самой таблице платежей формируется “отрицательный” платёж с “минус-суммой”. Таким образом, для определения баланса счёта клиента не надо будет создавать новой таблицы баланса. Кроме того эта схема позволит также решить вопрос с онлайн оплатой: при подключении агрегатора онлайн оплаты, системе нужно будет всего лишь создать платёж, равный сумме полученной оплаты. Этого будет достаточно для автоматического перевода Заказа в категорию оплаченного.
И предусматриваем работу с платежами по банку:
Всё, коллеги! С планированием покончено и переходим к превращению только что скачанного HTML-шаблона в “чек-лист” нашего проекта.
Итак, выбранный шаблон загружен с сайта Template Monster и распакован в отдельную папку. Если мы откроем в браузере файл index.html, то может показаться, что перед нами уже готовый проект: все (или почти все) кнопки и ссылки работают и ведут на нужные страницы. С одной, правда, существенной оговоркой: все эти красивые страницы наполнены не совсем той информацией, которая нам нужна. Следовательно, именно сейчас нам предстоит определить, какие именно страницы мы оставим, какие удалим, а какие, наоборот, добавим.
На этом выбор готовых страниц закончился - остальные страницы прошлось собрать с помощью "клея" и "ножниц" из того, было на удалённых страницах.
Кстати, для ссылок на страницы регистрации было использовано готовое меню выбора языков на главной страницы. Ссылки на следующие страницы были размещены в главном меню:
Ну, вот, кажется и всё: все основные моменты плана учтены, и все нужные страницы добавлены.
И в заключении темы планирования необходимо сказать об очень важным моменте: при планировании всегда нужно помнить ещё и о том, что план IT-проекта - это не священная корова. При реализации плана могут появиться детали, которые мы не предусмотрели вначале. Как, например, составляя план этого мини-курса, я не подумал о том, что второй урок получается совсем уж теоретическим. Поэтому, перенёс пункт “Выбор и доработка шаблона” из следующего урока в этот.
Резюме: если по мере реализации проекта возникла необходимость скорректировать план - корректируйте не задумываясь!
Более подробно с со всеми деталями этого этапа вы сможете познакомиться из этого видео:
Эта статья - представление нового курса Drop Shipping Store on Django, где за 7 занятий из бесплатных материалов, находящихся в открытом доступу, будет создан полноценный интернет-магазин, который может торговать чем угодно на условиях дропшиппинга. Если кто ещё не в курсе, то дропшиппинг - это способ организации бизнеса, при котором продавец размещает на своих виртуальных витринах товары, которых у него нет, но которые он в любой момент может получить у своего постоянного поставщика. Следовательно, задача владельца такого интернет-магазина: получить заказ и оплату от клиента, и затем передать поставщику информацию о заказе и полученную оплату за вычетом своей комиссии.
Всё, что потребуется для создания этого интернет-магазина: программы, необходимый учебные материалы и даже видео-уроки, находятся в открытом доступе. Вам лишь потребуется внимательно и аккуратно выполнять всё, что будет написано и показано в материалах к этому курсу.
Содержание курса:
Как уже упоминалось выше, все видео, подробно рассказывающие (а главное, показывающие!) процесс разработки, находятся в открытом доступе. Поэтому, внимательно следую рекомендациям на экране вы в итоге получаете реальный рабочий код всего проекта. Однако, для тех, кого сразу же интересует конечный результат, есть вариант получения доступа к архивной копии проекта после каждого урока. Этот доступ можно получить через небольшой донат вот по этой ссылке (потребуется регистрация на сайте).
Ну, а сам сайт, который получится в итоге, можно посмотреть на видео в конце этой статьи.
Для всех, кто интересуется веб разработкой, вне зависимости от уровня знаний и опыта!
Мы будем работать на операционной системе Linux, а в качестве IDE будем использовать Community (бесплатную) версию PyCharm. Выбор Linux неслучаен: именно на этой ОС работают все серверы, где используется фреймворк Django. Поэтому если Вы работаете на Windows, то лучшим решением будет дополнительно установить виртуальную машину с ОС Linux. О том, как это сделать подробно рассказано и показано в этих видео:
Кроме того, нам потребуется где-то сохранять проверенные и отлаженные версии нашего кода, а более удобного места чем репозиторий, для этой цели пока не придумано. Поэтому, если у вас ещё не создан аккаунт с SSH-доступом в каком-либо репозитории, то есть смысл ознакомиться вот с этой статьёй на нашем сайте, где подробно рассказано и показано, как создать на GutHub аккаунт и добавить в него SSH-ключи.
После проведения всех вышеперечисленных дополнительных приготовлений, мы наконец-то можем приступить непосредственно к созданию самого проекта в следующем порядке:
После выполнения всех пунктов этого чек-листа попробуем запустить проект. Ура - наш проект уже работает! И всё, что нам остаётся, - это "слегка" доработать его функционал!
Зайдём в админку под логином и паролем только что созданного суперпользователя. Как видим, здесь уже есть модели пользователей и групп пользователей.
Эту структуру мы ещё не добавляли, но она изначально находится в Django по умолчанию. Поэтому, при желании, первых пользователей для тестирования мы можем добавить ещё до того, как сделаем авторизацию пользователей на сайта. То есть прямо из админки!
Результаты всей этой проделанной подготовительной работы текущего этапа, вы можете найти по ссылке внизу видео, которая приведёт вас на курс Drop Shipping Store on Django. И, если курс у Вас оплачен, то Вы можете скачать архивную копию этого этапа (т. е. того, что сделано к настоящему моменту) (потребуется регистрация на сайте).
Более подробно с со всеми деталями этого этапа вы сможете познакомиться из этого видео: