Slide 1

Slide 1 text

РАЗРАБОТКА РАСШИРЯЕМЫХ DJANGO-ПРИЛОЖЕНИЙ Владимир Филонов

Slide 2

Slide 2 text

ЧТО ЭТО И ЗАЧЕМ? Расширяемость – возможность добавления функционала при помощи API, предоставляемого приложением Простой пример Простой пример AUTHENTICATION_BACKENDS в contrib.auth Решает проблемы: Повторное использование в различных условиях Изменение логики приложения, без вмешательства в основной код

Slide 3

Slide 3 text

DJANGO - РАСШИРЯЕМОЕ ПРИЛОЖЕНИЕ :) Любое приложение для django - по сути расширение функционала при помощи API. Благодаря этому, в django есть все необходимые инструменты и множество примеров django.utils.importlib.import_module django.utils.module_loading.module_has_submodule

Slide 4

Slide 4 text

ПРАКТИКУМ Представим, что нам надо разработать платформу Интернет-магазина # 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)

Slide 5

Slide 5 text

ПРАКТИКУМ #shop.models class 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)

Slide 6

Slide 6 text

ПРАКТИКУМ А что если нам понадобятся дополнительные услуги по заказам? Доставка – обязательно понадобиться Упаковка Еще что-нибудь Еще что-нибудь Причем, эти услуги могут быть разными, для разных ИМ на базе нашей платформы И мы даже не можем предсказать, какие именно

Slide 7

Slide 7 text

ОБОБЩИМ ТРЕБОВАНИЯ К УСЛУГЕ Название Описание Цена – может статичная, или зависеть от заказа Статус выполнения Дополнительная информация от клиента

Slide 8

Slide 8 text

КАК НАМ ВСЕ ЭТО ОРГАНИЗОВАТЬ? Заказ Услуга Услуга Диспетчер Бэкенд Бэкенд Заказ Услуга Услуга Диспетчер Бэкенд Бэкенд

Slide 9

Slide 9 text

ПРИВЯЗКА К ЗАКАЗУ - МОДЕЛЬ #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)

Slide 10

Slide 10 text

ПРИВЯЗКА К ЗАКАЗУ - МОДЕЛЬ # А можно и не хранить 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

Slide 11

Slide 11 text

САМОЕ ИНТЕРЕСНОЕ Итак, нам осталось сделать базовый класс для бэкенда и диспетчер Какой функционал нам понадобиться? Вычисление цены Получение, сохранение и обработка Получение, сохранение и обработка дополнительной информации Получение списка доступных статусов Реакция на смену статусов

Slide 12

Slide 12 text

БАЗОВЫЙ КЛАСС 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

Slide 13

Slide 13 text

#Построение списка бэкендов #Вариант первый – мы заранее знаем список плагинов #settings settings.SHOP_SERVICES_BACKENDS = { "simple_delivery" : "shop.services.delivery.SimpleDelivery" } #shop.utils def get_backends(init=False, initial_data=None): backends = [] И ДИСПЕТЧЕР 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

Slide 14

Slide 14 text

#Вариант второй – загрузка только тех модулей, которые указаны в БД def get_backends(init=False, initial_data=None): for service in Service.objects.all(): #Принцип тот же что и в первом варианте … И ДИСПЕТЧЕР

Slide 15

Slide 15 text

#Вариант третий – инспектирование модуля для поиска плагинов import inspect import pkgutil from 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: И ДИСПЕТЧЕР pkgutil.iter_modules(path=None, prefix='') Возвращает кортеж (module_loader, name, ispkg) для всех подмодулей import_module(name, package=None) Импортирует модуль. Удобство в том, что если 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 ispkg) для всех подмодулей Импортирует модуль. Удобство в том, что если передать имя начинающееся с точки ".name", то поиск для импорта будет производиться не по sys.path, а только в указанном во втором аргументе пакете. inspect.getmembers(object[, predicate]) Возвращает список всех членов объекта (аттрибуты, функции, классы и т.д.). Если качестве второго аргумента передать функцию-ограничитель, то inspect.getmembers вернет только те члены, для которых predicate вернет True

Slide 16

Slide 16 text

И ДИСПЕТЧЕР #Получение класса бэкенда по имени #Если бэкенда нет, мы можем или возвращать 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))

Slide 17

Slide 17 text

ПОПРОБУЕМ СОБРАТЬ ЭТО ВСЕ 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 = [] 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

Slide 18

Slide 18 text

ПОПРОБУЕМ СОБРАТЬ ЭТО ВСЕ 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 = { 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

Slide 19

Slide 19 text

ПОПРОБУЕМ СОБРАТЬ ЭТО ВСЕ #Шаблон #templates/shop/order_process.html {% extends "shop.html" %} {% block content %} {% csrf_token %} {{ order_form.as_p }} {% for service in services %}
{{ service.get_title }}
{{ service.get_description }}
{{ service.get_description }}
{% if service.has_form %} {{ service.get_form.as_p }} {% endif %}
{% endfor %} {% endblock %}

Slide 20

Slide 20 text

ЧТО ПОЛУЧИЛОСЬ?

Slide 21

Slide 21 text

СДЕЛАЕМ ПРОСТУЮ УСЛУГУ… #shop.services.simple_delivery class 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"Удобное время")

Slide 22

Slide 22 text

ЧТО ПОЛУЧИЛОСЬ?

Slide 23

Slide 23 text

А ТЕПЕРЬ ЕЩЕ ОДНУ class SingingCourier(BaseService): has_form = False keyword = "singing_courier" def get_title(self): return u"Поющий курьер" def get_description(self): return u"Курьер споет вам любую песню на ваш выбор"

Slide 24

Slide 24 text

ЧТО ПОЛУЧИЛОСЬ?

Slide 25

Slide 25 text

ЖМЕМ ОТПРАВИТЬ

Slide 26

Slide 26 text

С ЗАПОЛНЕННЫМИ ПОЛЯМИ

Slide 27

Slide 27 text

ПРОВЕРИМ ЧТО СОХРАНИЛОСЬ >>> from shop.models import Order >>> order = Order.objects.latest("id") >>> vars(order) {'customer': u'test', 'phone': u'', '_state': , 'id': 1, 'email': u'[email protected]'} >>> order.orderservice_set.count() 1 >>> service = order.orderservice_set.latest("id") >>> service.backend u'simple_delivery' >>> print json.loads(service.data) >>> print json.loads(service.data) {u'address': u'Москва, Малый Конюшковский переулок, дом 2', u'time': u'с 19 до 22'}

Slide 28

Slide 28 text

ЧТО ОСТАЛОСЬ? Интеграция с contrib.admin Редактирование данных Работа со статусами И еще много всего, но уже не сегодня =)

Slide 29

Slide 29 text

СПАСИБО! Email: [email protected] Email: [email protected] Код: https://bitbucket.org/VladimirFilonov/django-shop