Разработка расширяемых приложений на django
TRANSCRIPT
РАЗРАБОТКА РАСШИРЯЕМЫХ DJANGO-ПРИЛОЖЕНИЙВладимир Филонов
ЧТО ЭТО И ЗАЧЕМ?
Расширяемость – возможность добавления функционала при помощи API, предоставляемого приложением
Простой пример AUTHENTICATION_BACKENDS в contrib.auth
Решает проблемы: Повторное использование в различных
условиях Изменение логики приложения, без
вмешательства в основной код
DJANGO - РАСШИРЯЕМОЕ ПРИЛОЖЕНИЕ :)
Любое приложение для django - по сути расширение функционала при помощи API.
Благодаря этому, в django есть все необходимые инструменты и множество примеров
django.utils.importlib.import_module django.utils.module_loading.module_has_subm
odule
ПРАКТИКУМ
Представим, что нам надо разработать платформу Интернет-магазина
# 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)
ПРАКТИКУМ#shop.modelsclass Order(models.Model):
customer = models.CharField(max_length=128)
email = models.EmailField()
phone = models.CharField(max_length=32, blank=True, null=True)
class OrderItem(models.Model):
order = models.ForeignKey("Order")
item = models.ForeignKey("catalog.Product")
amount = models.PositiveSmallIntegerField(default=1)
price = models.DecimalField(max_digits=10, decimal_places=2)
ПРАКТИКУМ
А что если нам понадобятся дополнительные услуги по заказам? Доставка – обязательно понадобиться Упаковка Еще что-нибудь
Причем, эти услуги могут быть разными, для разных ИМ на базе нашей платформы
И мы даже не можем предсказать, какие именно
ОБОБЩИМ ТРЕБОВАНИЯ К УСЛУГЕ
Название Описание Цена – может статичная, или зависеть от
заказа Статус выполнения Дополнительная информация от клиента
КАК НАМ ВСЕ ЭТО ОРГАНИЗОВАТЬ?
Заказ
Услуга
Услуга
Услуга
Диспетчер
Бэкенд
Бэкенд
Бэкенд
ПРИВЯЗКА К ЗАКАЗУ - МОДЕЛЬ#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)
ПРИВЯЗКА К ЗАКАЗУ - МОДЕЛЬ# А можно и не хранить
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
САМОЕ ИНТЕРЕСНОЕ
Итак, нам осталось сделать базовый класс для бэкенда и диспетчер
Какой функционал нам понадобиться? Вычисление цены Получение, сохранение и обработка
дополнительной информации Получение списка доступных статусов Реакция на смену статусов
БАЗОВЫЙ КЛАСС
class BaseService(object): has_form = False
def __init__(self, order=None, data=None): self.data = data self.order = order
def get_title(self): return self.__class__.__name__
def get_description(self): return ""
def get_statuses(self): return []
def calculate_price(self, base_price): return base_price
def status_changed(self, old_status, new_status): pass def get_form(self): return None def get_template(self): return None
#Построение списка бэкендов#Вариант первый – мы заранее знаем список плагинов
#settingssettings.SHOP_SERVICES_BACKENDS = { "simple_delivery" : "shop.services.delivery.SimpleDelivery"}
#shop.utilsdef 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
И ДИСПЕТЧЕР
#Вариант второй – загрузка только тех модулей, которые указаны в БДdef get_backends(init=False, initial_data=None): for service in Service.objects.all(): #Принцип тот же что и в первом варианте …
И ДИСПЕТЧЕР
#Вариант третий – инспектирование модуля для поиска плагиновimport inspectimport pkgutilfrom django.utils.importlib import import_module
from shop import services
def get_backends(init=False, initial_data=None, as_list=True): if as_list: backends = [] else: backends = {} for mod in pkgutil.iter_modules(services.__path__): module = import_module('.{0}'.format(mod[1]), 'shop.services') 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: value = backend(data=initial_data) else: value = backend if as_list: backends.append(value) else: backends.update({backend.keyword: value}) return backends
И ДИСПЕТЧЕР
pkgutil.iter_modules(path=None, prefix='')
Возвращает кортеж (module_loader, name, ispkg) для всех подмодулей
import_module(name, package=None)Импортирует модуль. Удобство в том,
что если передать имя начинающееся с точки ".name", то поиск для импорта будет производиться не по sys.path, а
только в указанном во втором аргументе пакете.
inspect.getmembers(object[, predicate])Возвращает список всех членов объекта
(аттрибуты, функции, классы и т.д.). Если качестве второго аргумента
передать функцию-ограничитель, то inspect.getmembers вернет только те
члены, для которых predicate вернет True
И ДИСПЕТЧЕР#Получение класса бэкенда по имени#Если бэкенда нет, мы можем или возвращать Nonedef 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))
ПОПРОБУЕМ СОБРАТЬ ЭТО ВСЕ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
ПОПРОБУЕМ СОБРАТЬ ЭТО ВСЕ 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
ПОПРОБУЕМ СОБРАТЬ ЭТО ВСЕ#Шаблон#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 %}
ЧТО ПОЛУЧИЛОСЬ?
СДЕЛАЕМ ПРОСТУЮ УСЛУГУ…#shop.services.simple_deliveryclass SimpleDelivery (BaseService): has_form = True keyword = "simple_delivery"
def get_statuses(self): return ["planned", "in process", "done"]
def calculate_price(self, base_price, order): return base_price
def get_form_class(self): return SimpleDeliveryForm
def get_form(self): if not hasattr(self, "_form"): self._form = self.get_form_class()(self.data, prefix=self.__class__.__name__) return self._form
class SimpleDeliveryForm(forms.Form): address = forms.CharField(widget=forms.Textarea, label=u"Адрес", required=True) time = forms.CharField(label=u"Удобное время")
ЧТО ПОЛУЧИЛОСЬ?
А ТЕПЕРЬ ЕЩЕ ОДНУclass SingingCourier(BaseService): has_form = False keyword = "singing_courier"
def get_title(self): return u"Поющий курьер" def get_description(self): return u"Курьер споет вам любую песню на ваш выбор"
ЧТО ПОЛУЧИЛОСЬ?
ЖМЕМ ОТПРАВИТЬ
С ЗАПОЛНЕННЫМИ ПОЛЯМИ
ПРОВЕРИМ ЧТО СОХРАНИЛОСЬ>>> from shop.models import Order>>> order = Order.objects.latest("id")>>> vars(order){'customer': u'test', 'phone': u'', '_state': <django.db.models.base.ModelState object at 0x89607ec>, 'id': 1, 'email': u'[email protected]'}>>> order.orderservice_set.count()1>>> service = order.orderservice_set.latest("id")>>> service.backendu'simple_delivery'>>> print json.loads(service.data){u'address': u'Москва, Малый Конюшковский переулок, дом 2', u'time': u'с 19 до 22'}
ЧТО ОСТАЛОСЬ? Интеграция с contrib.admin Редактирование данных Работа со статусами И еще много всего, но уже не сегодня =)
СПАСИБО!Email: [email protected]
Код: https://bitbucket.org/VladimirFilonov/django-shop