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

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

Upload: moscowdjango

Post on 14-Jun-2015

1.720 views

Category:

Documents


6 download

TRANSCRIPT

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

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

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

ЧТО ЭТО И ЗАЧЕМ?

Расширяемость – возможность добавления функционала при помощи API, предоставляемого приложением

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

Решает проблемы: Повторное использование в различных

условиях Изменение логики приложения, без

вмешательства в основной код

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

DJANGO - РАСШИРЯЕМОЕ ПРИЛОЖЕНИЕ :)

Любое приложение для django - по сути расширение функционала при помощи API.

Благодаря этому, в django есть все необходимые инструменты и множество примеров

django.utils.importlib.import_module django.utils.module_loading.module_has_subm

odule

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

ПРАКТИКУМ

Представим, что нам надо разработать платформу Интернет-магазина

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

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

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

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

ПРАКТИКУМ

А что если нам понадобятся дополнительные услуги по заказам? Доставка – обязательно понадобиться Упаковка Еще что-нибудь

Причем, эти услуги могут быть разными, для разных ИМ на базе нашей платформы

И мы даже не можем предсказать, какие именно

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

ОБОБЩИМ ТРЕБОВАНИЯ К УСЛУГЕ

Название Описание Цена – может статичная, или зависеть от

заказа Статус выполнения Дополнительная информация от клиента

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

КАК НАМ ВСЕ ЭТО ОРГАНИЗОВАТЬ?

Заказ

Услуга

Услуга

Услуга

Диспетчер

Бэкенд

Бэкенд

Бэкенд

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

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

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

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

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

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

САМОЕ ИНТЕРЕСНОЕ

Итак, нам осталось сделать базовый класс для бэкенда и диспетчер

Какой функционал нам понадобиться? Вычисление цены Получение, сохранение и обработка

дополнительной информации Получение списка доступных статусов Реакция на смену статусов

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

БАЗОВЫЙ КЛАСС

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

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

#Построение списка бэкендов#Вариант первый – мы заранее знаем список плагинов

#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

И ДИСПЕТЧЕР

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

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

И ДИСПЕТЧЕР

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

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

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

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

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

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

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

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

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

ПОПРОБУЕМ СОБРАТЬ ЭТО ВСЕ#Шаблон#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 %}

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

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

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

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

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

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

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

А ТЕПЕРЬ ЕЩЕ ОДНУclass SingingCourier(BaseService): has_form = False keyword = "singing_courier"

def get_title(self): return u"Поющий курьер" def get_description(self): return u"Курьер споет вам любую песню на ваш выбор"

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

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

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

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

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

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

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

ПРОВЕРИМ ЧТО СОХРАНИЛОСЬ>>> 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'}

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

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