Установка и настройка окружения¶
Установить зависимости:
pip install m3-core m3-ext3 objectpack django==1.4
Если django-проект ещe не создан, то создаем и в
INSTALLED_APS
добавляем приложения:INSTALLED_APPS = ( ... 'm3_ext', 'm3_ext.ui', 'objectpack', )
#TODO: Подключить desktop view (м.б. сделать ссылку на m3-ext3)
Инициализировать контроллер:
# controller.py from objectpack.observer import ObservableController, Observer observer = Observer() controller = ObservableController(url="actions", observer=observer)
#TODO: Расширить urlpatterns
PROFIT!
Подробно про ObjectPack¶
ObjectPack
- это пак, который реализует основные
CRUD операции для модели и содержит следующие экшены:
ObjectListWindowAction
- возвращает окно со списком объектовObjectRowsAction
- возвращает JSON-строки для окна со списком объектовObjectAddWindowAction
- возвращает ExtJS окно добавления нового объектаObjectEditWindowAction
- возвращает ExtJS окно редактирования объектаObjectSaveAction
- сохранение нового и обновление существующего объектовObjectDeleteAction
- удаляет объекты
Для того чтобы сконфигурировать свой пак, минимально требуется лишь указать модель, по которой он будет строиться. Так предыдущий пример можно было записать как:
# actions.py
class PersonPack(ObjectPack):
model = Person
# разрешим добавлять ссылку на list_window в меню Desktop'а
add_to_menu = True
Всё по прежнему работает, но вместо колонок с полями модели в гриде отображается всего одна колонка “Наименование” и пропали кнопки Добавить/Редактировать/Удалить. Так мы получили простой список объектов.
Список объектов¶
Настройки колонок в окне со списком объектов хранятся в атрибуте columns
.
По умолчанию он имеет значение:
columns = [
{
'data_index': '__unicode__',
'header': u'Наименование',
}
]
columns
- это список словарей, где каждый словарь соответствует одной
колонке. Колонки в гриде будут расположены в том же порядке.
data_index¶
Для задания колонки достаточно задать ключ data_index, значением которого
могут быть атрибут объекта, property или callable объект, который
можно вызвать без передачи аргументов. Так же можно получить доступ к атрибутам
доступным через композицую, например 'userprofile.user.username'
.
prepare_row¶
Можно указать значение несуществующего атрибута. В ObjectPack
есть метод
prepare_row
, который
позвоялет установить дополнительные атрибуты в объект перед сериализацией в
JSON:
# actions.py
class PersonPack(ObjectPack):
model = Person
columns = [
{
'data_index': '__unicode__',
'header': u'Имя',
},
{
'data_index': 'birthday',
'header': u'Дата рождения',
},
{
'data_index': 'is_adult',
'header': u'Достигнул совершеннолетия',
}
]
def prepare_row(self, obj, request, context):
today = datetime.date.today()
is_adult = ((today - obj.birthday).days // 365 >= 18)
obj.is_adult = '<div class="x-grid3-check-col%s"/>' % (
'-on' if is_adult else '')
return obj
Сортировка и поиск¶
Чтобы включить поиск и сортировку по колонке, нужно добавить в columns
:
columns = [
{
'data_index': '__unicode__',
'header': u'Имя',
'searchable': True,
'search_fields': ('name', 'surname'),
'sortable': True,
# Сортировка сперва по имени, потом по фамилии
'sort_fields': ('name', 'surname'),
}
]
Значениями по ключам search_fields
и sort_fields
должнен быть кортеж из
лукапов полей django модели. Например:
{
'search_fields': (
'userprofile__user__username',
'userprofile__person__name',
'userprofile__person__surname')
}
Примечание
Если data_index
колонки соотвествует полям модели, то ключи
search_fields
и sort_fields
можно опустить.
Установкой атрибута list_sort_order
можно задать сортировку по умолчанию:
class PersonPack(ObjectPack):
list_sort_order = ('name', 'surname')
Фильтрация на сервере¶
Часто бывает необходимо ограничить изначальную выборку данных.
Для этого необходимо в паке перегрузить метод
get_rows_query
:
def get_rows_query(self, request, context):
query = super(PersonPack, self).get_rows_query(request, context)
query = query.filter(birthday__isnull=False)
return query
Колоночные фильтры¶
Иногда общей строки поиска по гриду бывает недостаточно и нужны отдельные фильтры по колонкам. В objectpack есть два вида колоночных фильтров: встроенные в контекстное меню заголовка колонки и контролы расположенные непросредтвенно в заголовке. По умолчанию включен первый тип. Рассмотрим на примере:
class PersonPack(ObjectPack):
model = Person
columns = [
{
'data_index': '__unicode__',
'header': u'Фамилия Имя',
'width': 2,
'filter': {
'type': 'string',
'custom_fields': ('name', 'surname')
}
},
{
'data_index': 'gender',
'header': u'Пол',
'width': 1,
'filter': {
'type': 'list',
'options': model.GENDERS
}
},
{
'data_index': 'birthday',
'header': u'Дата рождения',
'width': 1,
'filter': {
'type': 'date',
}
}
]
from functools import partial
from objectpack.filters import ColumnFilterEngine, FilterByField
class PersonPack(objectpack.ObjectPack):
model = models.Person
filter_engine_clz = ColumnFilterEngine
f = partial(FilterByField, model)
columns = [
{
'data_index': '__unicode__',
'header': u'Фамилия Имя',
'width': 2,
'filter': (
f('name', 'name__icontains')
& f('surname', 'surname__icontains')
)
},
{
'data_index': 'gender',
'header': u'Пол',
'width': 1,
'filter': f('gender')
},
{
'data_index': 'birthday',
'header': u'Дата рождения',
'width': 2,
'filter': (
f('birthday', 'birthday__gte', tooltip=u'С')
& f('birtday', 'birthday__lte', tooltip=u'По')
)
}
]
Окно со списком объектов¶
По умолчанию в качестве окна со списком объектов используется
BaseListWindow
. Отнаследовавшись от него
можно конфигурировать свои окна со списками или можно перегрузить методы пака
create_list_window
и get_list_window_params
.
Создание объекта¶
Теперь добавим в наш справочник возможность создавать новые объекты.
Окно добавления¶
Для этого необходимо установить в атрибут add_window
класс окна.
Это может быть любой класс унаследованный от
BaseEditWindow
.
Этот класс реализует каркас для окна и предоставляет некоторый интерфейс, который следует соблюдать:
from objectpack.ui import BaseEditWindow, make_combo_box
from m3_ext.ui import all_components as ext
from models import Person
class PersonAddWindow(BaseEditWindow):
def _init_components(self):
"""
Здесь следует инициализировать компоненты окна и складывать их в
:attr:`self`.
"""
super(PersonAddWindow, self)._init_components()
self.field__name = ext.ExtStringField(
label=u'Имя',
name='name',
allow_blank=False,
anchor='100%')
self.field__surname = ext.ExtStringField(
label=u'Фамилия',
name='surname',
allow_blank=False,
anchor='100%')
self.field__gender = make_combo_box(
label=u'Пол',
name='gender',
allow_blank=False,
anchor='100%',
data=Person.GENDERS)
self.field__birthday = ext.ExtDateField(
label=u'Дата рождения',
name='birthday',
anchor='100%')
def _do_layout(self):
"""
Здесь размещаем компоненты в окне
"""
super(PersonAddWindow, self)._do_layout()
self.form.items.extend((
self.field__name,
self.field__surname,
self.field__gender,
self.field__birthday,
))
def set_params(self, params):
"""
Установка параметров окна
:params: Словарь с параметрами, передается из пака
"""
super(PersonAddWindow, self).set_params(params)
self.height = 'auto'
Теперь скажем паку какое окно нужно использовать:
class PersonPack(ObjectPack):
model = Person
add_window = ui.PersonAddWindow
...
Генерация окон¶
Описыние компонент окна занятие утомительное и скушное. К счастью в objectpack есть убер-фича - генерация окон редактирования для модели. Так окно из предыдущего примера полностью идентично слудующему:
from objectpack import ModelEditWindow
add_window = ModelEditWindow.fabricate(model=Person)
Тонкая настройка окон¶
Часто бывает нужно дополнительно сконфигурировать окно, особенно это актуально
в случае с генерированными окнами. Для этого удобно использовать два метода в
паке: create_edit_window
и get_edit_window_params
Редактирование¶
Теперь добавим возможность редактировать объекты. Для этого нужно паку задать
атрибут edit_window
. В нашем случае окно редактирования идентично окну
создания, поэтому мы пишем:
add_window = edit_window = ModelEditWindow.fabricate(model=Person)
Окно редактирирование может быть сложным, например, когда у модели есть зависимые
модели. В таких случаях можно использовать окно с вкладками
TabbedWindow
.
Конфигурирование окна осуществляется так же как и для окна создания.
Сохранение¶
ObjectSaveAction
будет доступен в паке после задания либо окна создания,
либо окна редактирования объекта.
При сохранении значения из формы окна добавления/редактирования сопоставляются
с полями модели по атрибутам name
элементов формы.
Непосредственное сохранение объекта модели происходит в методе
save_row
. Перегрузив этот метод
можно дополнительно управлять сохранением объекта:
def save_row(self, obj, create_new, request, context):
if not (obj.name.isalpha() and obj.surname.isalpha()):
raise ApplicationLogicException(
u'Имя и Фамилимя могут содержать только буквы алфавита!')
super(PersonPack, self).save_row(obj, create_new, request, context)
Удаление¶
За удаление объекта отвечает атрибут can_delete
, который может принимать
три значения: True
, False
или None
. По умолчанию None
.
Если установлено значение None
, то ObjectDeleteAction
будет добавлен в пак
если задоно либо окно добавления, либо окно редактирования. True
удаление
возможно и False
- не возможно:
class PersonPack(ObjectPack):
model = Person
can_delete = True
Само удаление объекта модели происходит в методе
delete_row
. По умолчанию тут
вызывается метод safe_delete
модели и, если он не определен, вызывается
функция m3.db.safe_delete()
. Перегрузив его можно управлять удалением
объекта:
def delete_row(self, obj_id, request, context):
if date.today().weekday() in (5, 6):
raise ApplicationLogicException(
u'Нельзя удалять записи в выходные дни!')
# не хотим использовать m3.db.safe_delete
obj = self.model.objects.get(id=obj_id)
obj.delete()
return obj
Контроллер, наблюдатель и точки расширения¶
В качестве контроллера в ObjectPack используется ObservableController
.
Особенностью этого контроллера является то, что при регистрации в нём экшена,
последний в свою очередь добавляется в реестр слушателей наблюдателя Observer
.
Observer¶
Observer
позволяет регистрировать в экшенах точки расширения,
а также добавляет в каждый экшен две точки расширения before и after, которые
действуют как m3.actions.Action.pre_run
и m3.actions.Action.post_run
,
но выполняются соответственно до и после них, т.е. если методы before и after
вернут какой-либо результат ActionResult, то результатом выполнения экшена будет он.
Рассмотрим на примере из objectpack.demo
:
# создаём наблюдателя
obs = observer.Observer()
#logger=logger, verbose_level=observer.Observer.LOG_MORE)
# создаём контроллер
action_controller = observer.ObservableController(obs, "/controller")
Далее создаем слушателя, который описывается классом с одним обязательным атрибутом listen:
@obs.subscribe
class Listener(object):
# список регулярок, для сопоставления экшенам
listen = ['.*/.*/ObjectListWindowAction']
def after(self, request, context, response):
response.data.title = u'Му-ха-ха! %s' % response.data.title
Так мы подменили текст заголовка окна, метод after слушателя будет вызван после post_run экшена.
Примечание
response - это по сути ActionResult, а мы помним что,
ExtUIScriptResult в атрибуте data хранит ExtJS компонент, в данном
случае это будет объект окна objectpack.ui.BaseListWindow
.
Помимо before и after в экшенах ObjectPack’a, зарегистрировано множество полезных точек расширения,
например prepare_obj для objectpack.actions.ObjectRowsAction
, которая делает тоже что и
objectpack.actions.ObjectPack.prepare_row
, только request и context здесь будут аттрибутами слушателя:
@obs.subscribe
class StarToHash(object):
listen = ['.*/BandedColumnPack/.*']
def prepare_obj(self, obj):
obj['field1'] = obj.get('field1', False) and (obj['id'] % 2)
return obj
Ниже приведен полный перечень точек расширения для ObjectPack, но ничего не мешает нам зарегистрировать свои:
class DoSomethingAction(objectpack.BaseAction):
def run(self, request, context):
message = 'Done'
self.handle('do_well', message)
return OperationResult(message=message)
@obs.subscribe
class DoSomethingListener(object):
listen = ['.*/.*/DoSomethingAction']
def do_well(self, message):
message = 'Well Done!'
return message
Результатом выполнения этого экшена будет информационное окошко с текстом Well Done!
Примечание
Если слушатели пишутся в одном приложении рядом с экшенами, то проще подключать их через декоратор. В случае если слушателей нужно подключить в другом модуле или в другом приложении, то лучше вынести их в отдельный модуль listeners.py и выполнить их регистрацию в app_meta.register_action. Регистировать можно либо через импорт модуля, если вы используете декоратор, или вызовом функции, которая будет подписывать слушателей в Observer
Когда могут понадобиться точки расширения?¶
Через точки расширения удобно делать проверки прав доступа и различных условий бизнес логики, тем самым можно разгрузить код экшена и делегировать эти проверки слушателю. Так же через точки расширения можно реализовать механизм плагинов.
Доступные точки расширения¶
Action | Точка расширения | Тип передаваемого объекта | Описание |
---|---|---|---|
ObjectRowsAction |
query | Выборка данных QuerySet | Манипуляции с выборкой данных из БД |
Следующие три точки технически ничем не отличается от query, но были вынесены отдельно, чтобы не нарушать семантику | |||
apply_search | Выборка данных QuerySet | Поиск по выборке | |
apply_filter | Выборка данных QuerySet | Фильтрация выборки | |
apply_sort_order | Выборка данных QuerySet | Сортировка выборки | |
get_rows | Список строк для сериализации в JSON | Манипуляция с готовым с сериалиазации списком | |
prepare_obj | Объект модели | Манипуляции с объектом перед сериализацией в JSON (например, установка доподнительных атрибутов) | |
row_editing | Кортеж с результатом редактирования ячейки: (Успешно/Неуспешно, Текст ошибки/None) | Обработка редактирования ячейки | |
ObjectDeleteAction |
TODO: пока нет | ||
ObjectSaveAction |
save_obj | Объект модели | Обработка сохранения модели. Слушатель
должен возбуждать исключение
AlreadySaved , если объект уже успешно
сохранён |
ObjectEditWindowAction
ObjectAddWindowAction |
set_window_params | Словарь с параметрами для передачи в окно | Манипуляции со словарём параметров для окна редактирования/добавления |
create_window | Объект окна (потомок
BaseWindow ) |
Манипуляции с компонентом окна ( добавление/удаление/редактирование различных элементов, установка параметров и т.д и т.п. | |
ObjectListWindowAction |
TODO: пока нет | ||
Все экшены | before | Принимает в аргументах request, context | Выполняется перед pre_run экшена |
after | Принимает в аргументах request, context и result | Выполняется после post_run экшена | |