2. ЧТО ЭТО И ЗАЧЕМ?
Расширяемость – возможность добавления
функционала при помощи
API, предоставляемого приложением
Простой пример
AUTHENTICATION_BACKENDS в contrib.auth
Решает проблемы:
Повторное использование в различных условиях
Изменение логики приложения, без
вмешательства в основной код
3. DJANGO - РАСШИРЯЕМОЕ ПРИЛОЖЕНИЕ :)
Любое приложение для django - по сути
расширение функционала при помощи API.
Благодаря этому, в django есть все необходимые
инструменты и множество примеров
django.utils.importlib.import_module
django.utils.module_loading.module_has_submodule
4. ПРАКТИКУМ
Представим, что нам надо разработать
платформу Интернет-магазина
# catalog.models
class Category(models.Model):
title = models.CharField(max_length=32)
slug = models.SlugField(max_length=32 , unique=True)
class Product(models.Model):
title = models.CharField(max_length=32)
slug = models.SlugField(max_length=32, unique=True)
category = models.ForeignKey("Category")
price = models.DecimalField(max_digits=10, decimal_places=2)
6. ПРАКТИКУМ
А что если нам понадобятся дополнительные
услуги по заказам?
Доставка – обязательно понадобиться
Упаковка
Еще что-нибудь
Причем, эти услуги могут быть разными, для
разных ИМ на базе нашей платформы
И мы даже не можем предсказать, какие именно
7. ОБОБЩИМ ТРЕБОВАНИЯ К УСЛУГЕ
Название
Описание
Цена – может статичная, или зависеть от заказа
Статус выполнения
Дополнительная информация от клиента
8. КАК НАМ ВСЕ ЭТО ОРГАНИЗОВАТЬ?
Услуга Бэкенд
Заказ Услуга Диспетчер Бэкенд
Услуга Бэкенд
9. ПРИВЯЗКА К ЗАКАЗУ - МОДЕЛЬ
#shop.models
class OrderService(models.Model):
order = models.ForeignKey("Order")
service = models.ForeignKey("Service")
status = models.CharField(max_length=32, blank=True, default="")
data = models.TextField() #Мы будем хранить данные в JSON
# Можно хранить сервисы в базе
class Service(models.Model):
title = models.CharField(max_length=32)
description = models.TextField()
base_price = models.DecimalField(max_digits=10, decimal_places=2)
backend = models.CharField(max_length=32)
active = models.BooleanField(default=False)
10. ПРИВЯЗКА К ЗАКАЗУ - МОДЕЛЬ
# А можно и не хранить
class OrderService(models.Model):
order = models.ForeignKey("Order")
backend = models.CharField(max_length=32)
status = models.CharField(max_length=32, blank=True, default="")
data = models.TextField() #Мы будем хранить данные в JSON
11. САМОЕ ИНТЕРЕСНОЕ
Итак, нам осталось сделать базовый класс для
бэкенда и диспетчер
Какой функционал нам понадобиться?
Вычисление цены
Получение, сохранение и обработка
дополнительной информации
Получение списка доступных статусов
Реакция на смену статусов
13. И ДИСПЕТЧЕР
#Построение списка бэкендов
#Вариант первый – мы заранее знаем список плагинов
#settings
settings.SHOP_SERVICES_BACKENDS = {
"simple_delivery" : "shop.services.delivery.SimpleDelivery"
}
#shop.utils
def get_backends(init=False, initial_data=None):
backends = []
for backend_key in settings.SHOP_SERVICES_BACKENDS:
try:
path = settings.SHOP_SERVICES_BACKENDS[backend_key]
i = path.rfind('.')
module, attr = path[:i], path[i+1:]
mod = import_module()
cls = getattr(mod, attr)
if init:
backends.append(cls(data=initial_data))
else:
backends.append(cls)
except ImportError:
continue
return backends
14. И ДИСПЕТЧЕР
#Вариант второй – загрузка только тех модулей, которые указаны в БД
def get_backends(init=False, initial_data=None):
for service in Service.objects.all():
#Принцип тот же что и в первом варианте
…
15. И ДИСПЕТЧЕР
#Вариант третий – инспектирование модуля для поиска плагинов
import inspect
import pkgutil
from django.utils.importlib import import_module
from shop import services
def get_backends(init=False,pkgutil.iter_modules(path=None, prefix='')
initial_data=None, as_list=True):
if as_list: Возвращает кортеж
backends = [] import_module(name, package=None)
else:
(module_loader, name, ispkg) для всех
backends = {}
Импортирует модуль. Удобство в том, что если
подмодулей
передать имя начинающееся с точки
inspect.getmembers(object[, predicate])
for mod in pkgutil.iter_modules(services.__path__):
Возвращаетто поисквсех членов объекта
".name", список для импорта будет
module = import_module('.{0}'.format(mod[1]), 'shop.services')
производиться не по sys.path, а только в
predicate = lambda x: inspect.isclass(x) and issubclass(x, services.BaseService) and not
(аттрибуты, функции, классы и т.д.). Если
x == services.BaseService
указанном во втором аргументе пакете.
for name, backend in inspect.getmembers(module,аргумента передать
качестве второго predicate):
if init: функцию-ограничитель, то inspect.getmembers
value = backend(data=initial_data) те члены, для которых predicate
вернет только
else:
value = backend вернет True
if as_list:
backends.append(value)
else:
backends.update({backend.keyword: value})
return backends
16. И ДИСПЕТЧЕР
#Получение класса бэкенда по имени
#Если бэкенда нет, мы можем или возвращать None
def get_backend(name, init=False, initial_data=None):
return get_backends(init, initial_data).get(name)
#Или же
def get_backend(name, init=False, initial_data=None):
backend = get_backends(init, initial_data) .get(name)
if not backend:
raise ImproperlyConfigured(u"There is no service backend named `{0}`".format(name))
17. ПОПРОБУЕМ СОБРАТЬ ЭТО ВСЕ
class ProcessOrderView(View):
def get(self, *args, **kwargs):
context = {
"order_form": OrderForm(),
"services": get_backends(init=True)
}
return self.render_to_response(context)
def get_services(self):
if not hasattr(self, "_submitted_services"):
services = []
for service_name in self.request.POST.getlist("service"):
service = get_backend(service_name, init=True, initial_data=self.request.POST)
services.append(service)
self._submitted_services = services
return self._submitted_services
def all_services_valid(self):
valid = True
for service in self.get_services():
if not service.get_form().is_valid():
valid = False
return valid
18. ПОПРОБУЕМ СОБРАТЬ ЭТО ВСЕ
def post(self, *args, **kwargs):
order_form = OrderForm(self.request.POST)
valid = True
if order_form.is_valid() and self.all_services_valid():
order = order_form.save()
for service in self.get_services():
form_data = json.dumps(service.get_form().cleaned_data)
OrderService.objects.create(order=order, backend=service.keyword, data=form_data)
return HttpResponseRedirect("/shop/success/")
else:
valid = False
if not valid:
services = self.get_filled_services()
context = {
"order_form": order_form,
"services": services
}
return self.render_to_response(context)
def get_filled_services(self):
services = []
for service in get_backends():
if service.keyword in self.request.POST.getlist("service"):
service.checked = True
services.append(service(data=self.request.POST))
else:
service.checked = False
services.append(service)
return services
19. ПОПРОБУЕМ СОБРАТЬ ЭТО ВСЕ
#Шаблон
#templates/shop/order_process.html
{% extends "shop.html" %}
{% block content %}
<form method="POST">{% csrf_token %}
{{ order_form.as_p }}
{% for service in services %}
<div class="service {{ service.keyword }}">
<input type="checkbox" name="service" value="{{ service.keyword }}"{% if
service.checked %} checked{% endif %}><label>{{ service.get_title }}</label>
<div><small>{{ service.get_description }}</small></div>
{% if service.has_form %}
{{ service.get_form.as_p }}
{% endif %}
</div>
{% endfor %}
<input type="submit">
</form>{% endblock %}
23. А ТЕПЕРЬ ЕЩЕ ОДНУ
class SingingCourier(BaseService):
has_form = False
keyword = "singing_courier"
def get_title(self):
return u"Поющий курьер"
def get_description(self):
return u"Курьер споет вам любую песню на ваш выбор"