Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Разработка расширяемых приложений

Разработка расширяемых приложений

Владимир Филонов

На примере небольшой части интернет-магазина рассмотрели различные варианты реализации API для подключения плагинов к django-приложениям. Подробно, много кода, но всё понятно и логично выстроено. Хороший доклад, думаем, при разработке сложных систем со слабым связыванием компонентов всем придётся столкнуться с чем-либо подобным.

Moscow Python Meetup
PRO

May 10, 2012
Tweet

More Decks by Moscow Python Meetup

Other Decks in Programming

Transcript

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

    View Slide

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

    View Slide

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

    View Slide

  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)

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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)

    View Slide

  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

    View Slide

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

    View Slide

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

    View Slide

  13. #Построение списка бэкендов
    #Вариант первый – мы заранее знаем список плагинов
    #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

    View Slide

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

    И ДИСПЕТЧЕР

    View Slide

  15. #Вариант третий – инспектирование модуля для поиска плагинов
    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

    View Slide

  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))

    View Slide

  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 = []
    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

    View Slide

  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 = {
    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

    View Slide

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

    service.checked %} checked{% endif %}>{{ service.get_title }}
    {{ service.get_description }}
    {{ service.get_description }}
    {% if service.has_form %}
    {{ service.get_form.as_p }}
    {% endif %}

    {% endfor %}

    {% endblock %}

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  27. ПРОВЕРИМ ЧТО СОХРАНИЛОСЬ
    >>> from shop.models import Order
    >>> order = Order.objects.latest("id")
    >>> vars(order)
    {'customer': u'test', 'phone': u'', '_state': 0x89607ec>, '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'}

    View Slide

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

    View Slide

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

    View Slide