Внимание! Если у вас возникли проблемы с выполнением предыдущего этапа, то вы сможете зайти в соответствующий урок, скачать архив предыдущего этапа, инсталлировать его, и начать этот урок именно с того самого места, где закончился предыдущий!
Содержание курса:
Конечно же, самым удобным (и, кстати, более надёжным) способом будет взаимодействие нашего сайта с сайтом-поставщиком по 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. И, если курс у Вас оплачен, то Вы можете скачать архивную копию этого этапа (т. е. того, что сделано к настоящему моменту) (потребуется регистрация на сайте).
Более подробно с со всеми деталями этого этапа вы сможете познакомиться из этого видео:
Для тех, кто ещё не знаком со связанными (зависимыми) выпадающими списками и пока не успел оценить по достоинству тот невероятный набор удобств, который они дают при работе с электронными таблицами, рекомендую познакомиться с этой статьёй. (Там же есть ссылка на видео)
Те же, кому очевидные достоинства связанных выпадающих списков рекламировать без надобности, рано или поздно "упираются" в естественные, и увы, неизбежные ограничения электронных таблиц:
Особенно это актуально для владельцев малого бизнеса, хорошо знакомых с симптомами "болезни роста": то, что невероятно помогало ещё вчера стало мешать и тормозить сегодня.
С этой целью и была предпринята попытка совместить достоинства электронных таблиц и базы данных. И местом для подобного "полигона" была выбрана административная панель популярного фреймоворка Django, написанного на языке программирования Python.
Главным аргументом при этом выборе стала административная панель этого фреймворка. Админка Django имеет табличную структуру, так что внешнее сходство с электронным таблицами уже налицо. Поэтому доработки до уровня соответствия заданным параметрам здесь требуются самые минимальные.
Ну, а такие приятные "мелочи", как невероятная популярность этого фреймворка, его надёжность и производительность просто укрепили в этом выборе.
Python - дитя Unix. Поэтому, выбор unix-подобной операционной системы (Linux или macOS) будет совершенно логичным решением. Тем более, что переходить на компьютер с другой OC совсем не обязательно: можно просто установить на своём компьютере с любой операционной системой виртуальную машину и установить Linux уже на неё. А также установить PyCharm - удобную и бесплатную среду разработки.
О том, как это сделать подробно рассказано и показано в этих видео:
После успешной установки всего вышеперечисленного можно переходить к созданию проекта. Для этого надо создать каталог с названием проекта django-dropdown-admin, войти в этот каталог и запустить процесс создания виртуального окружения и тут же его запустить:
$ virtualenv venv - p python3
$ source venv/bin/activate
Виртуальное окружение запущено, о чём говорит имя папки в скобках, появившееся в начале командной строки: (venv).
Далее устанавливаем Django:
(venv) $ pip install django
Создаём проект (обратите внимание на точку в конце!):
(venv) $ django-admin startproject main .
И приложение dropdown:
(venv) $ django-admin startapp dropdown
В результате должно получиться что-то вроде этого:
.
├── dropdown
│ ├── admin.py
│ ├── apps.py
│ ├── __init__.py
│ ├── migrations
│ │ └── __init__.py
│ ├── models.py
│ ├── tests.py
│ └── views.py
├── main
│ ├── asgi.py
│ ├── __init__.py
│ ├── settings.py
│ ├── urls.py
│ └── wsgi.py
├── manage.py
└── venv
С этого момента все дальнейшие действия удобнее будет выполнять в IDE PyCharm. (см видео в конце этой статьи)
Прежде всего внесём изменения в настройки - файл main/settings.py:
1.) Добавим вновь созданное приложение в INSTALLED_APPS
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
# === my apps =======
'dropdown',
]
2.) Отменим сложную валидацию паролей пользователей для режима отладки
# Password validation
# https://docs.djangoproject.com/en/4.0/ref/settings/#auth-password-validators
if not DEBUG:
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
3.) И укажем путь для файлов статики (потребуется для изменения js админки)
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/4.0/howto/static-files/
STATIC_URL = 'static/'
STATICFILES_DIRS = [
BASE_DIR / "static",
]
Чтобы логически завершить последнее действие, сразу же добавим в директорию проекта новый каталог для файлов статики: static
Как известно в Django используется ORM (Object–relational mapping), поэтому для создания БД необходимо всего-навсего описать её структуру в файлеmodels.py и затем провести миграции, которые, по описанию этой структуры, создадут необходимые таблицы базы данных и установят между полями этих таблиц все необходимые связи.
Содержимое файла dropdown/models.py:
from django.db import models
class Category(models.Model):
name_cat = models.CharField(max_length=250)
def __str__(self):
return self.name_cat
class Subcategory(models.Model):
cat = models.ForeignKey(Category, on_delete=models.CASCADE,
verbose_name='category')
name_subcat = models.CharField(max_length=250)
def __str__(self):
return self.name_subcat
class Good(models.Model):
subcat = models.ForeignKey(Subcategory, on_delete=models.CASCADE,
verbose_name='subcategory')
name_good = models.CharField(max_length=250)
price = models.DecimalField(max_digits=6, decimal_places=2)
def __str__(self):
return self.name_good
class OrderItem(models.Model):
order = models.ForeignKey("dropdown.Order", on_delete=models.CASCADE, verbose_name="order")
cat = models.ForeignKey(Category, on_delete=models.CASCADE, verbose_name='cat')
subcat = models.ForeignKey(Subcategory, on_delete=models.CASCADE, verbose_name='subcat')
name_good = models.ForeignKey(Good, on_delete=models.CASCADE, verbose_name='good')
quantity = models.PositiveIntegerField(default=0)
amount = models.DecimalField(max_digits=9, decimal_places=2)
def __str__(self):
return f'{self.name_good} + {self.quantity}'
class Order(models.Model):
order_id = models.PositiveIntegerField(unique=True)
order_date = models.DateTimeField(auto_now=True)
total_quantity = models.PositiveIntegerField(default=0)
total_amount = models.DecimalField(max_digits=9, decimal_places=2)
def __str__(self):
return str(self.order_id)
class AllowedCombination(models.Model):
cat = models.ForeignKey(Category, on_delete=models.CASCADE)
subcat = models.ForeignKey(Subcategory, on_delete=models.CASCADE)
good = models.ForeignKey(Good, on_delete=models.CASCADE)
def __str__(self):
return f'{self.cat} {self.subcat} {self.good}'
class Meta:
ordering = ['pk']
Теперь, когда структура БД описана, необходимо её воплотить в самой БД, для чего мы сначала в окне терминала первой командой создаём миграции (python manage.py makemigrations):
(venv) $ python manage.py makemigrations
Migrations for 'dropdown':
dropdown/migrations/0001_initial.py
- Create model Category
- Create model Good
- Create model Order
- Create model Subcategory
- Create model OrderItem
- Add field subcat to good
- Create model AllowedCombination
А затем, следующей командой (python manage.py migrate), применяем все эти миграции к нашей базе данных:
(venv) $ python manage.py migrate
Operations to perform:
Apply all migrations: admin, auth, contenttypes, dropdown, sessions
Running migrations:
Applying contenttypes.0001_initial... OK
Applying auth.0001_initial... OK
Applying admin.0001_initial... OK
Applying admin.0002_logentry_remove_auto_add... OK
Applying admin.0003_logentry_add_action_flag_choices... OK
Applying contenttypes.0002_remove_content_type_name... OK
Applying auth.0002_alter_permission_name_max_length... OK
Applying auth.0003_alter_user_email_max_length... OK
Applying auth.0004_alter_user_username_opts... OK
Applying auth.0005_alter_user_last_login_null... OK
Applying auth.0006_require_contenttypes_0002... OK
Applying auth.0007_alter_validators_add_error_messages... OK
Applying auth.0008_alter_user_username_max_length... OK
Applying auth.0009_alter_user_last_name_max_length... OK
Applying auth.0010_alter_group_name_max_length... OK
Applying auth.0011_update_proxy_permissions... OK
Applying auth.0012_alter_user_first_name_max_length... OK
Applying dropdown.0001_initial... OK
Applying sessions.0001_initial... OK
В самом простейшем варианте, мы можем работать с нашей базой данных прямо из Админки. Кстати, это тот редкий случай, когда "простейшее" совсем не означает "худшее". Как раз наоборот - стандартная админка Django уже содержит в себе невероятно удобные и мощные инструменты для добавления, изменения и удаления данных из БД.
И всё, что для этого нужно - это все лишь сделать минимальное описание структуры Админки в файле dropdown/admin.py с помощью следующего кода:
from django.contrib import admin
from .models import (AllowedCombination, Category, Good, Order, OrderItem,
Subcategory)
admin.site.register(Category)
admin.site.register(Subcategory)
admin.site.register(Good)
class OrderItemInline(admin.TabularInline):
model = OrderItem
extra = 0
class OrderAdmin(admin.ModelAdmin):
inlines = [OrderItemInline]
admin.site.register(Order, OrderAdmin)
class OrderItemAdmin(admin.ModelAdmin):
pass
admin.site.register(OrderItem, OrderItemAdmin)
class AllowedCombinationAdmin(admin.ModelAdmin):
list_display = ['cat', 'subcat', 'good', ]
admin.site.register(AllowedCombination, AllowedCombinationAdmin)
Для входа в Админку осталось сделать последний шаг - создать Суперпользователя.
Поэтому снова входим в окно терминала и сначала вводим команду создания Суперпользователя (python manage.py createsuperuser), а затем последовательно отвечаем на вопросы системы:
(venv) $ python manage.py createsuperuser
Username (leave blank to use 'su'): root
Email address: root@root.com
Password:
Password (again):
Superuser created successfully.
Теперь самое время проверить, что у нас получилось. Запускаем сайт с помощью команды python manage.py runserver
(venv) $ python manage.py runserver
Watching for file changes with StatReloader
Performing system checks...
System check identified no issues (0 silenced).
April 06, 2022 - 17:33:16
Django version 4.0.3, using settings 'main.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.
и переходим в админку по url: http://127.0.0.1:8000/admin/
И логинимся как Суперпользователь, используя login и password, которые ввели при созданиии пользователя superuser.
В нашем варианты будет три связанных выпадающих списка:
Необходимо сначала заполнить их, а потом заполнить таблицу Допустимых Комбинаций (AllowedCombination) на основании которой наш скрипт и будет предлагать значения в выпадающих списках. Ну, что-то вроде этого:
Разумеется, в каждом конкретном случае все таблицы, имена категорий и подкатегорий, глубина вложения всех этих категорий-подкатегорий-под-подкатегорий, и, конечно же, сами значения, будут совершенно различными. Предложенный вариант - всего лишь иллюстрация самой идеи.
Всё, что было до этого - самый, что на есть, обычный и стандартный Django. Который умеет работать со списками, но пока ещё не умеет делать их связанными, то есть предлагать значения последующего списка с оглядкой на выбор предыдущего.
Чтобы научить Django новому функционалу, надо добавить немного "магии", а именно:
Добавляем файл dropdown/forms.py:
import json
from typing import Any
from django import forms
from django.forms import Media, widgets
from .models import Good, OrderItem, Subcategory
class RestrictedSelect(widgets.Select):
@property
def media(self):
media = super().media
media += Media(js=["js/restricted-model-choice-field.js"])
return media
class BoundRestrictedModelChoiceField(forms.BoundField):
def get_restrictions(self):
restrictions = {}
restrict_on_form_field = self.form.fields[self.field.restrict_on_form_field]
# Можно оптимизировать
for restricting_object in restrict_on_form_field.queryset:
allowed_objects = self.field.queryset.filter(**{self.field.restrict_on_relation: restricting_object})
for obj in allowed_objects:
restrictions.setdefault(obj.id, set()).add(restricting_object.id)
return restrictions
def build_widget_attrs(self, attrs, widget=None):
attrs = super().build_widget_attrs(attrs, widget)
restrictions = self.get_restrictions()
restrictions = {k: [str(v) for v in vs] for k, vs in restrictions.items()}
attrs["data-restrictions"] = json.dumps(restrictions)
bound_restrict_on_form_field = self.form[self.field.restrict_on_form_field]
attrs["data-restricted-on"] = bound_restrict_on_form_field.html_name
return attrs
class RestrictedModelChoiceField(forms.ModelChoiceField):
widget = RestrictedSelect
def __init__(self, *args, restrict_on_form_field: str = None, restrict_on_relation: str = None, **kwargs):
super().__init__(*args, **kwargs)
if not restrict_on_form_field:
raise ValueError("restrict_on_form_field is required")
self.restrict_on_form_field = restrict_on_form_field
if not restrict_on_relation:
raise ValueError("restrict_on_relation is required")
self.restrict_on_relation = restrict_on_relation
def get_bound_field(self, form, field_name):
return BoundRestrictedModelChoiceField(form, self, field_name)
class OrderItemForm(forms.ModelForm):
class Meta:
model = OrderItem
fields = "__all__"
subcat = RestrictedModelChoiceField(
Subcategory.objects.all(),
restrict_on_form_field="cat",
restrict_on_relation="allowedcombination__cat",
)
name_good = RestrictedModelChoiceField(
Good.objects.all(),
restrict_on_form_field="subcat",
restrict_on_relation="allowedcombination__subcat",
)
Добавляем файл статики static/js/restricted-model-choice-field.js:
(function(){
function throwError(baseElement, message) {
console.error(message, baseElement);
throw new Error(message);
}
function reset(baseElement) {
baseElement.value = "";
}
function getRestrictedFields() {
return Array.prototype.slice.apply(document.querySelectorAll("[data-restrictions]"));
}
function getFieldsRestrictedOn(baseElement) {
let elements = getRestrictedFields();
return elements.filter(e => getRestrictedOnField(e) === baseElement);
}
function isFormsetTemplate(baseElement) {
return baseElement.name.indexOf("__prefix__") >= 0;
}
function getRestrictedOnField(baseElement) {
if (isFormsetTemplate(baseElement)) {
return null;
}
let fieldName = baseElement.getAttribute("data-restricted-on");
if (!fieldName) {
throwError(baseElement, "data-restricted-on is undefined");
}
if (fieldName.indexOf("__prefix__") >= 0) {
fieldName = cleanDynamicFormSetName(baseElement, fieldName);
}
let form = baseElement.closest("form");
if (!form) {
throwError(baseElement, "The field is not inside a form");
}
let fields = form.querySelectorAll(`[name=${fieldName}]`);
if (fields.length == 0) {
throwError(baseElement, `Could not find field ${fieldName}`);
}
if (fields.length > 1) {
console.warn(`Found multiple fields ${fieldName}`);
}
return fields[0];
}
function cleanDynamicFormSetName(baseElement, fieldName) {
let prefixIx = fieldName.indexOf("__prefix__");
let selfPrefix = baseElement.name.slice(prefixIx);
let prefixMatch = selfPrefix.match(/\d+/);
if (!prefixMatch) {
throwError(baseElement, `Cannot detect dynamic formset prefix: ${baseElement.name}`);
}
return fieldName.replace("__prefix__", prefixMatch[0]);
}
function getRestrictions(baseElement) {
let restrictionsJson = baseElement.getAttribute("data-restrictions");
if (!restrictionsJson) {
throwError(baseElement, "data-restrictions is undefined");
}
return JSON.parse(restrictionsJson);
}
function updateOptionList(baseElement) {
if (isFormsetTemplate(baseElement)) {
return;
}
let refField = getRestrictedOnField(baseElement);
if (!refField) {
throwError(baseElement, "Could not find refField");
}
let restrictions = getRestrictions(baseElement);
let options = Array.prototype.slice.apply(baseElement.querySelectorAll("option"));
options.forEach(option => {
if (!option.value) {
option.hidden = false;
return;
}
let allowedOnValues = restrictions[option.value] || [];
option.hidden = allowedOnValues.indexOf(refField.value) < 0;
});
}
function clearOptionList(baseElement) {
let options = Array.prototype.slice.apply(baseElement.querySelectorAll("option"));
options.forEach(option => {
option.hidden = true;
});
}
document.addEventListener("change", event => {
let element = event.target;
getFieldsRestrictedOn(element).forEach(baseElement => {
reset(baseElement);
updateOptionList(baseElement);
baseElement.dispatchEvent(new Event("change", {bubbles: true}));
});
});
document.addEventListener("DOMContentLoaded", () => {
getRestrictedFields().forEach(baseElement => {
if (isFormsetTemplate(baseElement)) {
clearOptionList(baseElement);
} else {
updateOptionList(baseElement);
}
});
})
})();
И вносим изменения в уже существующий файл dropdown/admin.py:
from django.contrib import admin
from .forms import OrderItemForm
from .models import (AllowedCombination, Category, Good, Order, OrderItem,
Subcategory)
admin.site.register(Category)
admin.site.register(Subcategory)
admin.site.register(Good)
class OrderItemInline(admin.TabularInline):
model = OrderItem
form = OrderItemForm
extra = 0
class OrderAdmin(admin.ModelAdmin):
inlines = [OrderItemInline]
admin.site.register(Order, OrderAdmin)
class OrderItemAdmin(admin.ModelAdmin):
form = OrderItemForm
admin.site.register(OrderItem, OrderItemAdmin)
class AllowedCombinationAdmin(admin.ModelAdmin):
list_display = ['cat', 'subcat', 'good', ]
admin.site.register(AllowedCombination, AllowedCombinationAdmin)
Ву а-ля! Теперь в нашей Админке при заполнении таблицы Заказа (Order) мы можем использовать Связанные Выпадающие списки:
А общий список файлов нашего проекта выглядит теперь следующим образом:
.
├── db.sqlite3
├── dropdown
│ ├── admin.py
│ ├── apps.py
│ ├── forms.py
│ ├── __init__.py
│ ├── migrations
│ │ ├── 0001_initial.py
│ │ └── __init__.py
│ ├── models.py
│ ├── __pycache__
│ │ ├── admin.cpython-38.pyc
│ │ ├── apps.cpython-38.pyc
│ │ ├── __init__.cpython-38.pyc
│ │ └── models.cpython-38.pyc
│ ├── tests.py
│ └── views.py
├── main
│ ├── asgi.py
│ ├── __init__.py
│ ├── __pycache__
│ │ ├── __init__.cpython-38.pyc
│ │ ├── settings.cpython-38.pyc
│ │ ├── urls.cpython-38.pyc
│ │ └── wsgi.cpython-38.pyc
│ ├── settings.py
│ ├── urls.py
│ └── wsgi.py
├── manage.py
├── static
│ └── js
│ └── restricted-model-choice-field.js
└── venv
Более подробно о создание этого проекта рассказано (и, самое главное, показано!) в этом видео:
Для того, чтобы получить доступ к репозиторию на ГитХабе по ssh, прежде всего необходимо создать сам аккаунт.
Далее, после авторизации необходимо перейти в настройки аккаунта (кликнуть на значок своего аватара в правом верхнему углу экрана и выбрать пункт settings) --> SSH and GPG keys), и далее кликнуть кнопку New SSH key (тоже справа вверху).
Теперь в эти два открывшихся поля (Title и Key) необходимо вставить название ключа (любое) и код публичной части самого ключа (скопировать содержимое ключа).
Где их взять? Прежде всего есть смысл проверить, не сгенерировали ли вы свой ключ раньше и просто забыли об этом. Для этого надо перейти в папку Home и поискать там ключи в скрытой папке .ssh (предварительно в Навигаторе надо добавить возможность просмотра скрытых файлов). Другой способ обнаружить эту папку - через окно терминала по команде:
$ ls -al ~/.ssh
Если ключи есть, то мы увидим, как минимум, 4 строки текста
drwx------ 2 su su 4096 сеп 24 23:24 .
drwxr-xr-x 23 su su 4096 сеп 24 23:24 ..
-rw------- 1 su su 419 сеп 24 23:24 id_ed25519
-rw-r--r-- 1 su su 104 сеп 24 23:24 id_ed25519.pub
Ну, а если нет - то только две верхних. В этом случае, ключи нам придётся сгенерировать самостоятельно:
$ ssh-keygen -t ed25519 -C "my_email@example.com"
Система сообщит о начале процесса генерации и уточнит, под каким именем следует сохранить файл с ключом:
Generating public/private ed25519 key pair.
Enter file in which to save the key (/home/su/.ssh/id_ed25519):
Просто соглашаемся (Enter).
Далее система сообщит о создании директории .ssh и предложит ввести фразу для пароля (вводить не обязательно):
Created directory '/home/su/.ssh'.
Enter passphrase (empty for no passphrase):
Если ввели пароль, то следующим шагом потребуется ввести подтверждение, если - нет, то просто нажмите Enter.
И в результате пароль будет сформирован и на экран будет выведено что-то вроде этого:
Enter same passphrase again:
Your identification has been saved in /home/su/.ssh/id_ed25519
Your public key has been saved in /home/su/.ssh/id_ed25519.pub
The key fingerprint is:
SHA256:FOkr66ZIZN5eTZ1omlY77s1GkgZtWa+A3NNDwEKCzcE my_email@example.com
The key's randomart image is:
+--[ED25519 256]--+
| =oo..oo |
| . E.. o.o |
| . *.= . |
| +.Ooo.. |
| o oS=oo |
| + . .B=.o |
| o . =++o |
| . o oo. +. |
| . o+..o.o |
+----[SHA256]-----+
Теперь необходимо перейти в папку .ssh и скопировать публичную часть ключа, которая находится в файле с расширением: .pud (id_ed25519.pub).
Содержимое именно этого файла нам надо скопировать и вставить в поле Key экрана настройки ключей на ГитХабе. Далее необходимо придумать имя для этого ключа (на случай если ключей будет несколько) и ввести это имя в поле Title. После заполнения обоих полей и нажатия кнопки Add SSH key, добавление ключа можно считать завершённым. И теперь неплохо бы было убедиться, что всё работает.
Для этого вводим в окне терминала команду тестового подключения к ГитХаб:
ssh -T git@github.com
Если ssh-ключи находятс в правильном месте, а публичный ключ корректно скопирован в настройки ГитХаб, то будет что-то вроде этого:
"Hi UserName!
You've successfully authenticated, but GitHub does not provide shell access."
Теперь мы создадим в нашем репозитории тестовый проект, скопируем его на локальную машину, попробуем внести в наш проект изменения и "запушить" результат в репозиторий.
Нажимает "плюс" слева от аватара в верхнем правом углу ГитХаба, между "колокольчиком" и аватаром --> New repository
Далее, в открывшейся странице вводим имя проекта под Repository name, пишем несколько слов о проекте в разделе Description, отмечаем "добавить" для Add a README file и Add .gitignore, и в вариантах ".gitignore" выбираем Python. И в завершении всего нажимаем Create repository.
Проект создан, и теперь мы копируем его в папку на локальном компьютере, для чего в переключателе HTTPS / SSH выбираем последнее и копируем ссылку на наш только что созданный проект в репозитории ГитХаба.
Но перед тем, как скопировать, необходимо убедиться, установлен ли на нашим компьютере пакет git. Это можно сделать командой:
$ git --version
Если же окажется, что git не установлен, то применяем следующие команды;
$ sudo apt-get update
$ sudo apt-get install git
Теперь, непосредственно в той папке, где мы хотим разместить этот проект, открываем окно терминала (правая кнопка мыши --> "Open in Terminal"), и далее, в самом терминале вводим:
$ git clone git@github.com:it4each/my-project.git
При первом клонировании скорее всего мы получим что-то вроде этого:
Cloning into 'my-project'...
The authenticity of host 'github.com (140.82.121.4)' can't be established.
RSA key fingerprint is SHA256:nThbg6kXUpJWGl7E1IGOCspRomTxdCARLviKw6E5SY8.
Are you sure you want to continue connecting (yes/no/[fingerprint])?
Отвечаем yes и проект успешно добавляется в указанную папку на нашем компьютере. Кроме того, адрес ГитХаба добавился в список известных хостов (см. далее).
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added 'github.com,140.82.121.4' (RSA) to the list of known hosts.
remote: Enumerating objects: 4, done.
remote: Counting objects: 100% (4/4), done.
remote: Compressing objects: 100% (3/3), done.
remote: Total 4 (delta 0), reused 0 (delta 0), pack-reused 0
Receiving objects: 100% (4/4), done.
И если мы теперь ещё раз исследуем содержимое папку ключей, то увидим в ней новый файл (known_hosts) с со списком известных хостов, куда добавлен и хост ГитХаба:
$ ls -al ~/.ssh
drwx------ 2 su su 4096 сеп 25 10:49 .
drwxr-xr-x 23 su su 4096 сеп 24 23:39 ..
-rw------- 1 su su 411 сеп 24 23:43 id_ed25519
-rw-r--r-- 1 su su 102 сеп 24 23:43 id_ed25519.pub
-rw-r--r-- 1 su su 884 сеп 25 10:49 known_hosts
Теперь, для работы проекта нам необходимо создать виртуальное окружение. Для этого необходимо наличие пакетов pip3 и virtualenv. Если эти пакеты ещё не установлены, используем команды:
# install pip3
$ sudo apt-get install python3-pip
# install virtual environment
$ sudo pip3 install virtualenv
Для создания виртуального окружения необходимо перейти в папку проекта и затем запустить команду установки:
$ cd my-project
$ virtualenv venv -p python3
Виртуальное окружение создано. Теперь его необходимо запустить:
user@user:~/Projects/django-app/my-project$ source venv/bin/activate
(venv) user@user:~/Projects/django-app/my-project$
Доказательство того, что виртуальное окружение запущено - появление в ответе системы папки venv в круглых скобках - (venv).
Устанавливаем первый пакет - пакет Django. И сразу же после его установки создаём новый проект (чтобы создать его внутри папки проекта, в конец команды добавляем точку):
$ pip3 install Django
$ django-admin startproject main .
Все установленные пакеты запоминаем в файле requirements.txt:
$ pip3 freeze > requirements.txt
Теперь в нашей папке уже достаточно файлов для того, чтобы сделать первый коммит и сохранить проделанную работу в репозитории ГитХаба. Для этого сначала добавляем все созданные файлы командой add --all, затем создаём коммит commit и отправляем этот коммит в репозиторий командой push:
$ git add --all
$ git commit -am "first commit"
$ git push
Если всё прошло без ошибок, то в репозиторий должны добавиться следующие файлы:
[main 0a792bb] first commit
7 files changed, 200 insertions(+)
create mode 100644 main/__init__.py
create mode 100644 main/asgi.py
create mode 100644 main/settings.py
create mode 100644 main/urls.py
create mode 100644 main/wsgi.py
create mode 100755 manage.py
create mode 100644 requirements.txt
Обратите внимание, что самая большая по объёму папка нашего проекта - папка виртуального окружения venv в репозиторий не попала, поскольку по умолчанию находится в файле .gitignore - в списке файлов-исключений для копирования в удалённое хранилище. Если помните, добавить этот файл нам предложил ГитХаб при создании проекта my-project.
Следует заметить, что папки .idea для служебных файлов PyCharm в .gitignore нет. Поэтому, если вы будете использовать в создании вашего проекта эту IDE - не забудьте добавить исключение папки .idea самостоятельно.
Дополнительную информацию о добавлении SSH ключа и создании проекта можно получить из этого видео:
Это продолжение статьи Исключения в Python. Введение
Бывают ситуации, когда формально ошибки нет, но переменная принимает такое значение, при котором программа может дать результат, очень далёкий от ожидания. И тогда мы можем вызвать ошибку принудительно, с помощью оператора raise:
try:
value = int(input())
if value % 2 != 0:
raise ValueError('Variable <value> must be even!')
print('Congratulations! Variable <value> is even!')
except Exception as e:
print(f'{e.__class__}: {e}')
В приведённом примере исключение будет вызвано в том случае, когда целое число не является чётным, то есть в случае выполнения условия if value % 2 != 0:
Для этого после оператора raise указывается класс исключения (в данном случае это ValueError), к которому будет отнесена соответствующая группа значений переменной value. А далее, в скобках, добавлен комментарий, поясняющий причину вызова исключения. Ниже приводятся результаты выполнения кода для значений 4 и 5:
>>>
5
<class 'ValueError'>: Variable <value> must be even!
4
'Congratulations! Variable <value> is even!'
Если появится другая ошибка, например, если с консоли будет введено значение aaa, то программа перейдёт на обработку этого исключения ещё до оператора raise. И, разумеется, выдаст совершенно другой код ошибки (точнее, выдаст другой комментарий ошибки к общему классу исключений ValueError):
>>>
aaa
<class 'ValueError'>: invalid literal for int() with base 10: 'aaa'
Ещё одна возможность принудительно вызвать ошибку, в случае невыполнения указанного условия — с помощью оператора assert:
try:
value = int(input())
assert value % 2 == 0, 'Variable <value> must be even!'
print('Congratulations! Variable <value> is even!')
except Exception as e:
print(f'{e.__class__}: {e}')
Варианты выполнения кода для различных входных значений:
>>>
5
<class 'AssertionError'>: 'Variable <value> must be even!
4
'Congratulations! Variable <value> is even!'
aaa
<class 'ValueError'>: invalid literal for int() with base 10: 'aaa'
Таким образом, как видим, оба оператора (raise и assert) очень похожи друг на друга.
Разве что вариант с assert выглядит самую малость предпочтительней, так как:
Это продолжение статьи Исключения в Python. Введение
Иногда определённые типы исключений требуют специфической реакции или специфической обработки, отличной от обработки других типов исключений. Python предоставляет такую возможность - возможность классифицировать исключение по имени класса и совершить необходимые действия, заранее определённые для каждого выбранного класса ошибок.
Но прежде чем перейти к примеру кода, который решает этот вопрос, стоит посмотреть, какие вообще существуют классы исключений в Python. Для этого в первую очередь следует обратиться к официальной документации Python: https://docs.python.org/3/library/exceptions.html .
Можно также получить список всех встроенных исключений с помощью следующей команды в консоли Python:
dir(locals()['__builtins__'])
В коде, представленном ниже, в отдельные блоки формируются 3 группы исключений:
while True:
line = input('Please enter an integer: ')
if line == 'end':
break
try:
result = 100 / int(line)
print(result)
except (ValueError, TypeError) as e:
print('Exception for Value or Type Error')
print(e.__class__, e)
print('Please enter another number! ')
except ZeroDivisionError as e:
print('Exception for ZeroDivisionError')
print(e.__class__, e)
print('Please enter another number! ')
except Exception as e:
print('Exception for other error')
print(e.__class__, e)
print('Please enter another number! ')
Продолжить в статье Исключения в Python. Операторы вызова исключений: raise и assert
Вернуться в начало, на статью Исключения в Python. Введение