ݺߣ

ݺߣShare a Scribd company logo
РАЗРАБОТКА РАСШИРЯЕМЫХ
DJANGO-ПРИЛОЖЕНИЙ
Владимир Филонов
ЧТО ЭТО И ЗАЧЕМ?
  Расширяемость – возможность добавления
   функционала при помощи
   API, предоставляемого приложением

  Простой пример
   AUTHENTICATION_BACKENDS в contrib.auth


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

 django.utils.importlib.import_module
 django.utils.module_loading.module_has_submodule
ПРАКТИКУМ
   Представим, что нам надо разработать
    платформу Интернет-магазина

# 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.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)
ПРАКТИКУМ
   А что если нам понадобятся дополнительные
    услуги по заказам?
     Доставка – обязательно понадобиться
     Упаковка
     Еще что-нибудь


 Причем, эти услуги могут быть разными, для
  разных ИМ на базе нашей платформы
 И мы даже не можем предсказать, какие именно
ОБОБЩИМ ТРЕБОВАНИЯ К УСЛУГЕ
 Название
 Описание

 Цена – может статичная, или зависеть от заказа

 Статус выполнения

 Дополнительная информация от клиента
КАК НАМ ВСЕ ЭТО ОРГАНИЗОВАТЬ?


            Услуга                Бэкенд


  Заказ     Услуга    Диспетчер   Бэкенд


            Услуга                Бэкенд
ПРИВЯЗКА К ЗАКАЗУ - МОДЕЛЬ
#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
И ДИСПЕТЧЕР
#Построение списка бэкендов
#Вариант первый – мы заранее знаем список плагинов

#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
И ДИСПЕТЧЕР
#Вариант второй – загрузка только тех модулей, которые указаны в БД
def get_backends(init=False, initial_data=None):
  for service in Service.objects.all():
     #Принцип тот же что и в первом варианте
     …
И ДИСПЕТЧЕР
#Вариант третий – инспектирование модуля для поиска плагинов
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
И ДИСПЕТЧЕР
#Получение класса бэкенда по имени
#Если бэкенда нет, мы можем или возвращать 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))
ПОПРОБУЕМ СОБРАТЬ ЭТО ВСЕ
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_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"Удобное время")
ЧТО ПОЛУЧИЛОСЬ?
А ТЕПЕРЬ ЕЩЕ ОДНУ
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'example@example.com'}
>>> order.orderservice_set.count()
1
>>> service = order.orderservice_set.latest("id")
>>> service.backend
u'simple_delivery'
>>> print json.loads(service.data)
{u'address': u'Москва, Малый Конюшковский переулок, дом 2', u'time': u'с 19 до 22'}
ЧТО ОСТАЛОСЬ?
 Интеграция с contrib.admin
 Редактирование данных

 Работа со статусами

 И еще много всего, но уже не сегодня =)
СПАСИБО!
             Email: i@vladimir.filonov.name
Код: https://bitbucket.org/VladimirFilonov/django-shop

More Related Content

What's hot (17)

Andrew Borisenko "Magic of Vue.js""
Andrew Borisenko  "Magic of Vue.js""Andrew Borisenko  "Magic of Vue.js""
Andrew Borisenko "Magic of Vue.js""
OdessaJS Conf
Form api в drupal 7
Form api в drupal 7Form api в drupal 7
Form api в drupal 7
dimateus
Быть в 10 раз эффективнее благодаря Groovy
Быть в 10 раз эффективнее благодаря GroovyБыть в 10 раз эффективнее благодаря Groovy
Быть в 10 раз эффективнее благодаря Groovy
Evgeny Kompaniyets
Профилирование и оптимизация фреймворков высоконагруженных систем на примере ...
Профилирование и оптимизация фреймворков высоконагруженных систем на примере ...Профилирование и оптимизация фреймворков высоконагруженных систем на примере ...
Профилирование и оптимизация фреймворков высоконагруженных систем на примере ...
MageCloud
Подробная презентация JavaScript 6 в 1
Подробная презентация JavaScript 6 в 1Подробная презентация JavaScript 6 в 1
Подробная презентация JavaScript 6 в 1
Vasya Petrov
2014-08-02 01 Егор Непомнящих. jWidget - очередной MV*-фреймворк
2014-08-02 01 Егор Непомнящих. jWidget - очередной MV*-фреймворк2014-08-02 01 Егор Непомнящих. jWidget - очередной MV*-фреймворк
2014-08-02 01 Егор Непомнящих. jWidget - очередной MV*-фреймворк
Омские ИТ-субботники
[JAM 1.1] Clean Code (Paul Malikov)
[JAM 1.1] Clean Code (Paul Malikov)[JAM 1.1] Clean Code (Paul Malikov)
[JAM 1.1] Clean Code (Paul Malikov)
Evgeny Kaziak
Enterprise Patterns in Magento
Enterprise Patterns in MagentoEnterprise Patterns in Magento
Enterprise Patterns in Magento
Vrann Tulika
Perl: Symbol table
Perl: Symbol tablePerl: Symbol table
Perl: Symbol table
Elena Shishkina
Лекция #7. Django ORM
Лекция #7. Django ORMЛекция #7. Django ORM
Лекция #7. Django ORM
Яковенко Кирилл
Подробная презентация JavaScript 6 в 1
Подробная презентация JavaScript 6 в 1Подробная презентация JavaScript 6 в 1
Подробная презентация JavaScript 6 в 1
Vasya Petrov
Эффективное программирование на NodeJS
Эффективное программирование на NodeJSЭффективное программирование на NodeJS
Эффективное программирование на NodeJS
Yura Bogdanov
Производительность в Django
Производительность в DjangoПроизводительность в Django
Производительность в Django
MoscowDjango
Web осень 2013 лекция 4
Web осень 2013 лекция 4Web осень 2013 лекция 4
Web осень 2013 лекция 4
Technopark
Agile Instrumentation
Agile InstrumentationAgile Instrumentation
Agile Instrumentation
Mikalai_Kardash
Andrew Borisenko "Magic of Vue.js""
Andrew Borisenko  "Magic of Vue.js""Andrew Borisenko  "Magic of Vue.js""
Andrew Borisenko "Magic of Vue.js""
OdessaJS Conf
Form api в drupal 7
Form api в drupal 7Form api в drupal 7
Form api в drupal 7
dimateus
Быть в 10 раз эффективнее благодаря Groovy
Быть в 10 раз эффективнее благодаря GroovyБыть в 10 раз эффективнее благодаря Groovy
Быть в 10 раз эффективнее благодаря Groovy
Evgeny Kompaniyets
Профилирование и оптимизация фреймворков высоконагруженных систем на примере ...
Профилирование и оптимизация фреймворков высоконагруженных систем на примере ...Профилирование и оптимизация фреймворков высоконагруженных систем на примере ...
Профилирование и оптимизация фреймворков высоконагруженных систем на примере ...
MageCloud
Подробная презентация JavaScript 6 в 1
Подробная презентация JavaScript 6 в 1Подробная презентация JavaScript 6 в 1
Подробная презентация JavaScript 6 в 1
Vasya Petrov
2014-08-02 01 Егор Непомнящих. jWidget - очередной MV*-фреймворк
2014-08-02 01 Егор Непомнящих. jWidget - очередной MV*-фреймворк2014-08-02 01 Егор Непомнящих. jWidget - очередной MV*-фреймворк
2014-08-02 01 Егор Непомнящих. jWidget - очередной MV*-фреймворк
Омские ИТ-субботники
[JAM 1.1] Clean Code (Paul Malikov)
[JAM 1.1] Clean Code (Paul Malikov)[JAM 1.1] Clean Code (Paul Malikov)
[JAM 1.1] Clean Code (Paul Malikov)
Evgeny Kaziak
Enterprise Patterns in Magento
Enterprise Patterns in MagentoEnterprise Patterns in Magento
Enterprise Patterns in Magento
Vrann Tulika
Подробная презентация JavaScript 6 в 1
Подробная презентация JavaScript 6 в 1Подробная презентация JavaScript 6 в 1
Подробная презентация JavaScript 6 в 1
Vasya Petrov
Эффективное программирование на NodeJS
Эффективное программирование на NodeJSЭффективное программирование на NodeJS
Эффективное программирование на NodeJS
Yura Bogdanov
Производительность в Django
Производительность в DjangoПроизводительность в Django
Производительность в Django
MoscowDjango
Web осень 2013 лекция 4
Web осень 2013 лекция 4Web осень 2013 лекция 4
Web осень 2013 лекция 4
Technopark

Similar to Разработка расширяемых приложений на Django (20)

Web весна 2012 лекция 7
Web весна 2012 лекция 7Web весна 2012 лекция 7
Web весна 2012 лекция 7
Technopark
Декораторы в Python и их практическое использование
Декораторы в Python и их практическое использование Декораторы в Python и их практическое использование
Декораторы в Python и их практическое использование
Sergey Schetinin
Web осень 2013 лекция 8
Web осень 2013 лекция 8Web осень 2013 лекция 8
Web осень 2013 лекция 8
Technopark
Разработка через тестирование в Python и Django #pyconru
Разработка через тестирование в Python и Django #pyconruРазработка через тестирование в Python и Django #pyconru
Разработка через тестирование в Python и Django #pyconru
JetStyle
Илья Шаляпин, Евгений Генералов: Разработка через тестирование в Python и Djn...
Илья Шаляпин, Евгений Генералов: Разработка через тестирование в Python и Djn...Илья Шаляпин, Евгений Генералов: Разработка через тестирование в Python и Djn...
Илья Шаляпин, Евгений Генералов: Разработка через тестирование в Python и Djn...
it-people
WebCamp: Developer Day: Parse'им бэкенд - Аким Халилов
WebCamp: Developer Day: Parse'им бэкенд - Аким ХалиловWebCamp: Developer Day: Parse'им бэкенд - Аким Халилов
WebCamp: Developer Day: Parse'им бэкенд - Аким Халилов
GeeksLab Odessa
ZFConf 2011: Разделение труда: Организация многозадачной, распределенной сист...
ZFConf 2011: Разделение труда: Организация многозадачной, распределенной сист...ZFConf 2011: Разделение труда: Организация многозадачной, распределенной сист...
ZFConf 2011: Разделение труда: Организация многозадачной, распределенной сист...
ZFConf Conference
Организация работы с API на Vue.js, Виталий Копачёв
Организация работы с API на Vue.js, Виталий КопачёвОрганизация работы с API на Vue.js, Виталий Копачёв
Организация работы с API на Vue.js, Виталий Копачёв
Mail.ru Group
Web весна 2013 лекция 4
Web весна 2013 лекция 4Web весна 2013 лекция 4
Web весна 2013 лекция 4
Technopark
Yii2
Yii2Yii2
Yii2
Noveo
JS утиліти WordPress на практиці
JS утиліти WordPress на практиціJS утиліти WordPress на практиці
JS утиліти WordPress на практиці
Shtrih Sruleg
Magento code debugging
Magento code debuggingMagento code debugging
Magento code debugging
aheadWorks
12 - Web-технологии. Django модели
12 - Web-технологии. Django модели12 - Web-технологии. Django модели
12 - Web-технологии. Django модели
Roman Brovko
Web осень 2013 лекция 6
Web осень 2013 лекция 6Web осень 2013 лекция 6
Web осень 2013 лекция 6
Technopark
Top 10 problems supporting Magento customers
Top 10 problems supporting Magento customersTop 10 problems supporting Magento customers
Top 10 problems supporting Magento customers
aheadWorks
Web осень 2012 лекция 4
Web осень 2012 лекция 4Web осень 2012 лекция 4
Web осень 2012 лекция 4
Technopark
10 - Web-технологии. MVC фреймворки (продолжение)
10 - Web-технологии. MVC фреймворки (продолжение)10 - Web-технологии. MVC фреймворки (продолжение)
10 - Web-технологии. MVC фреймворки (продолжение)
Roman Brovko
2015-12-06 Максим Юнусов - Проектирование REST приложения, или нужно ли прогр...
2015-12-06 Максим Юнусов - Проектирование REST приложения, или нужно ли прогр...2015-12-06 Максим Юнусов - Проектирование REST приложения, или нужно ли прогр...
2015-12-06 Максим Юнусов - Проектирование REST приложения, или нужно ли прогр...
HappyDev
Web весна 2012 лекция 7
Web весна 2012 лекция 7Web весна 2012 лекция 7
Web весна 2012 лекция 7
Technopark
Декораторы в Python и их практическое использование
Декораторы в Python и их практическое использование Декораторы в Python и их практическое использование
Декораторы в Python и их практическое использование
Sergey Schetinin
Web осень 2013 лекция 8
Web осень 2013 лекция 8Web осень 2013 лекция 8
Web осень 2013 лекция 8
Technopark
Разработка через тестирование в Python и Django #pyconru
Разработка через тестирование в Python и Django #pyconruРазработка через тестирование в Python и Django #pyconru
Разработка через тестирование в Python и Django #pyconru
JetStyle
Илья Шаляпин, Евгений Генералов: Разработка через тестирование в Python и Djn...
Илья Шаляпин, Евгений Генералов: Разработка через тестирование в Python и Djn...Илья Шаляпин, Евгений Генералов: Разработка через тестирование в Python и Djn...
Илья Шаляпин, Евгений Генералов: Разработка через тестирование в Python и Djn...
it-people
WebCamp: Developer Day: Parse'им бэкенд - Аким Халилов
WebCamp: Developer Day: Parse'им бэкенд - Аким ХалиловWebCamp: Developer Day: Parse'им бэкенд - Аким Халилов
WebCamp: Developer Day: Parse'им бэкенд - Аким Халилов
GeeksLab Odessa
ZFConf 2011: Разделение труда: Организация многозадачной, распределенной сист...
ZFConf 2011: Разделение труда: Организация многозадачной, распределенной сист...ZFConf 2011: Разделение труда: Организация многозадачной, распределенной сист...
ZFConf 2011: Разделение труда: Организация многозадачной, распределенной сист...
ZFConf Conference
Организация работы с API на Vue.js, Виталий Копачёв
Организация работы с API на Vue.js, Виталий КопачёвОрганизация работы с API на Vue.js, Виталий Копачёв
Организация работы с API на Vue.js, Виталий Копачёв
Mail.ru Group
Web весна 2013 лекция 4
Web весна 2013 лекция 4Web весна 2013 лекция 4
Web весна 2013 лекция 4
Technopark
JS утиліти WordPress на практиці
JS утиліти WordPress на практиціJS утиліти WordPress на практиці
JS утиліти WordPress на практиці
Shtrih Sruleg
Magento code debugging
Magento code debuggingMagento code debugging
Magento code debugging
aheadWorks
12 - Web-технологии. Django модели
12 - Web-технологии. Django модели12 - Web-технологии. Django модели
12 - Web-технологии. Django модели
Roman Brovko
Web осень 2013 лекция 6
Web осень 2013 лекция 6Web осень 2013 лекция 6
Web осень 2013 лекция 6
Technopark
Top 10 problems supporting Magento customers
Top 10 problems supporting Magento customersTop 10 problems supporting Magento customers
Top 10 problems supporting Magento customers
aheadWorks
Web осень 2012 лекция 4
Web осень 2012 лекция 4Web осень 2012 лекция 4
Web осень 2012 лекция 4
Technopark
10 - Web-технологии. MVC фреймворки (продолжение)
10 - Web-технологии. MVC фреймворки (продолжение)10 - Web-технологии. MVC фреймворки (продолжение)
10 - Web-технологии. MVC фреймворки (продолжение)
Roman Brovko
2015-12-06 Максим Юнусов - Проектирование REST приложения, или нужно ли прогр...
2015-12-06 Максим Юнусов - Проектирование REST приложения, или нужно ли прогр...2015-12-06 Максим Юнусов - Проектирование REST приложения, или нужно ли прогр...
2015-12-06 Максим Юнусов - Проектирование REST приложения, или нужно ли прогр...
HappyDev

More from MoscowDjango (10)

Тестирование и Django
Тестирование и DjangoТестирование и Django
Тестирование и Django
MoscowDjango
Пример fuzzy testing для поиска URL в тексте
Пример fuzzy testing для поиска URL в текстеПример fuzzy testing для поиска URL в тексте
Пример fuzzy testing для поиска URL в тексте
MoscowDjango
TDD или как я стараюсь писать код
TDD или как я стараюсь писать кодTDD или как я стараюсь писать код
TDD или как я стараюсь писать код
MoscowDjango
Cyclone + Eventsource (realtime push-сообщения)
Cyclone + Eventsource (realtime push-сообщения)Cyclone + Eventsource (realtime push-сообщения)
Cyclone + Eventsource (realtime push-сообщения)
MoscowDjango
Django на Android
Django на AndroidDjango на Android
Django на Android
MoscowDjango
Работа со статикой в Django
Работа со статикой в DjangoРабота со статикой в Django
Работа со статикой в Django
MoscowDjango
Class Based Generic Views в Django
Class Based Generic Views в DjangoClass Based Generic Views в Django
Class Based Generic Views в Django
MoscowDjango
Простой и удобный деплоймент проекта
Простой и удобный деплоймент проектаПростой и удобный деплоймент проекта
Простой и удобный деплоймент проекта
MoscowDjango
Django South. Миграция баз данных.
Django South. Миграция баз данных.  Django South. Миграция баз данных.
Django South. Миграция баз данных.
MoscowDjango
Журнальная вёрстка в Django
Журнальная вёрстка в DjangoЖурнальная вёрстка в Django
Журнальная вёрстка в Django
MoscowDjango
Тестирование и Django
Тестирование и DjangoТестирование и Django
Тестирование и Django
MoscowDjango
Пример fuzzy testing для поиска URL в тексте
Пример fuzzy testing для поиска URL в текстеПример fuzzy testing для поиска URL в тексте
Пример fuzzy testing для поиска URL в тексте
MoscowDjango
TDD или как я стараюсь писать код
TDD или как я стараюсь писать кодTDD или как я стараюсь писать код
TDD или как я стараюсь писать код
MoscowDjango
Cyclone + Eventsource (realtime push-сообщения)
Cyclone + Eventsource (realtime push-сообщения)Cyclone + Eventsource (realtime push-сообщения)
Cyclone + Eventsource (realtime push-сообщения)
MoscowDjango
Работа со статикой в Django
Работа со статикой в DjangoРабота со статикой в Django
Работа со статикой в Django
MoscowDjango
Class Based Generic Views в Django
Class Based Generic Views в DjangoClass Based Generic Views в Django
Class Based Generic Views в Django
MoscowDjango
Простой и удобный деплоймент проекта
Простой и удобный деплоймент проектаПростой и удобный деплоймент проекта
Простой и удобный деплоймент проекта
MoscowDjango
Django South. Миграция баз данных.
Django South. Миграция баз данных.  Django South. Миграция баз данных.
Django South. Миграция баз данных.
MoscowDjango
Журнальная вёрстка в Django
Журнальная вёрстка в DjangoЖурнальная вёрстка в Django
Журнальная вёрстка в Django
MoscowDjango

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

  • 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)
  • 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)
  • 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. САМОЕ ИНТЕРЕСНОЕ  Итак, нам осталось сделать базовый класс для бэкенда и диспетчер  Какой функционал нам понадобиться?  Вычисление цены  Получение, сохранение и обработка дополнительной информации  Получение списка доступных статусов  Реакция на смену статусов
  • 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
  • 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 %}
  • 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"Удобное время")
  • 23. А ТЕПЕРЬ ЕЩЕ ОДНУ class SingingCourier(BaseService): has_form = False keyword = "singing_courier" def get_title(self): return u"Поющий курьер" def get_description(self): return u"Курьер споет вам любую песню на ваш выбор"
  • 27. ПРОВЕРИМ ЧТО СОХРАНИЛОСЬ >>> 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'example@example.com'} >>> order.orderservice_set.count() 1 >>> service = order.orderservice_set.latest("id") >>> service.backend u'simple_delivery' >>> print json.loads(service.data) {u'address': u'Москва, Малый Конюшковский переулок, дом 2', u'time': u'с 19 до 22'}
  • 28. ЧТО ОСТАЛОСЬ?  Интеграция с contrib.admin  Редактирование данных  Работа со статусами  И еще много всего, но уже не сегодня =)
  • 29. СПАСИБО! Email: i@vladimir.filonov.name Код: https://bitbucket.org/VladimirFilonov/django-shop