Если в m3 2 для получения формы с данными используется один ajax-запрос на сервер. То в версии 3 будет последовательно генерироваться три запроса:
UI формируется по прежнему на сервере через python;
"Справочниковые" экшены поддерживаются только ObjectPack-ом. Поэтому классы наследоваемые от паков или экшенов
необходимо перевести на objectpack.
Подготовка к запуску продукта на новой версии:
Затем в несколько этапов нужно включать приложения в INSTALLED_APPS добавляя в TRASH.py новые классы-заглушки:
По мере включения что-то может ломаться, в таком случае необходимо дополнять файл TRASH.py новыми классами-заглушками и делать импорты на этот файл;
Главным отличием является то, что новой версией поддерживаются исключительно стандартные классы: Action и ActionPack. Для справочников и master-detail контейнеров необходимо использовать objectpack.
Для отдачи UI появился новый базовый класс UIAction Который включает в себя два метода:
{
"config" :: dict - базовый конфиг окна
"data" :: dict - базовые данные для инициализации окна
}
{
"ui": :: str - url для получения базового конфига окна
"model" :: dict - объект данных
"data": :: dict - данные для конкретного окна
}
В версии m3 3 отправляются три запроса при открытии нового окна (в порядке отправки):
Последовательность запросов при получении ui:
Таким образом необходимо во всех экшенах, которые отдают ui, изменить класс-наследник от UIAction. Экшены, не отдающие ui менять не нужно.
Под биндингом данных понимается автоматическое преобразование django-модели в extjs-форму и обратно. При условии, если названия атрибутов модели и формы совпадают.
Как было в версии m3 2 и раньше:
django model -> extjs form (при загрузки имеющихся данных, например, при редактировании)
В методе формы (ExtForm) был метод from_object, принимающий в качестве параметров объект модели, и производящий сопоставллениеполя формы с полями модели. Затем заполненное данными окно рендерилось через вызов метода render у окна. Таким образом получался js-код в совокупности с данными, который eval-лился на клиенте в браузере.
extjs form -> django model (при сабмите данных, например, при сохранении элемента справочника)
В методе формы (ExtForm) был метод bind_to_request, принимающий параметр - request. Данный метод производил заполнение формы.
В методе формы (ExtForm) был метод to_object, принимающий в качестве параметра объект модели, который необходимо заполнить данными из окна. Затем модель валидировалась и сохранялась.
Как стало в версии m3 3:
django model -> extjs form
При использовании ExtEditWindow достаточно соблюсти правило биндинга - название атрибутов модели и UI-окна должны совпадать. Если по неким причинам название не совпадают, то можно в js-представлении переопределить метод bind и внутри метода установить необходимый параметр или зависимость параметров.
extjs form -> django model
Не изменился.
При использовании objectpack появилась возможность указывать правила сериализации из объекта в словарь. Для этого
необходимо добавить метод serialize(include=None, exclude=None)
внутри которого объект модели доступен через self,
необходимо вернуть словарь. Внутри можно использовать функцию model_to_dict
для сериализации модели.
Так же крайне желательно проводить сериализацию внутри модели и тогда, когда objectpack не используется. В этом случае внутри метода get_result экшена необходимо у полученного объекта вызвать метод serialize. Например:
class RemaindersDocDetail(BasePaidServObjectModel): """ Табличная часть документа остатков """ remaindersdoc = models.ForeignKey(RemaindersDoc) consumer = models.ForeignKey("domain.Consumer") servicepoint = models.ForeignKey("domain.ServicePoint") service = models.ForeignKey("domain.Service", verbose_name=u'Услуга', blank=True, null=True) bank_props = models.ForeignKey('domain.BankProps', verbose_name=u'Платежные реквизиты', blank=True, null=True) type = models.SmallIntegerField(choices=RemaindersTypeEnum.get_choices(), null=False, default=RemaindersTypeEnum.DEBT) summa = models.DecimalField(null=False, blank=False, max_digits=16, decimal_places=2) out = models.BooleanField(verbose_name=u'Признак выбывшего', default=False) class Meta: db_table = 'finances_remainders_details' verbose_name = u"Табличная часть остатков" def serialize(self, *args): return model_to_dict(self) class RemaindersEditWindowAction(UIAction): """ Окно редактирования остатков """ url = '/edit-window' def context_declaration(self): return [ACD(name="remainders_detail_id", type=int, required=True, default=0), ACD(name="consumer_id", type=int, required=True, default=-1), ACD(name='sp_id', type=int, required=True, default=-1), ACD(name='period', type=int, required=True), ACD(name="global_provider_id", type=int, required=True)] def get_result(self, request, context): result = super(RemaindersEditWindowAction, self).get_result(request, context) if context.remainders_detail_id: detail = RemaindersDocDetail.objects.get(pk=context.remainders_detail_id) else: detail = RemaindersDocDetail() read_only = False if context.remainders_detail_id and detail.remaindersdoc.state == RemaindersDocStateEnum.REGISTERED: read_only = True if not urls.get_pack_instance('service-facts').has_sub_permission( request.user, urls.get_pack_instance('service-facts').REMAINDERS_PERMISSION, request ): read_only = True result['model'] = detail.serialize() result['data'].update({ 'read_only': read_only, 'submit_url': self.parent.save_action.absolute_url() }) return result def get_ui(self, request, context): return RemainderDetailEditWindow()
Важное замечание: в ui-экшенах декларация контекста производится так же как и раньше - через определение метода
context_declaration
, но он декларирует параметры исключительно для метода get_result
.
Так как метод get_ui
ничего не должен знать о контексте.
В версии m3 3 контекст может описываться с помощью словаря, пример:
def context_declaration(self): return { 'servicepoint_id': {'type': 'int'}, 'assigned_service_id': {'type': 'int_or_none', 'verbose_name': u"Идентификатор типовой услуги" } }
Это более предпочтительный пример, чем:
def context_declaration(self): return [ ACD(name='start', type=int, required=True, default=0), ACD(name='limit', type=int, required=True, default=25), ACD(name='date_since', type=date, required=False), ACD(name='date_until', type=date, required=False), ACD(name='filter', type=str, required=False), ACD(name='servicepoint_id', type=int, required=True, default=0), ]
Так как не нужно импортировать и использовать дополнительные классы, такие как ACD. К тому же удален параметр required, поэтому если указан параметр default - то параметр будет считаться не обязательным. ACD объявлен как deprecated.
С версии M3 3 поддерживается исключительно механизм работы через objectpack
со справочниками или с сущностями, которые
поддерживают операции создания/редактирования/удаления. То есть считаются устаревшими механизмы работы через классы
BaseDictionaryActions
, BaseDictionaryModelActions
, BaseTreeDictionaryActions
,
BaseTreeDictionaryModelActions
.
Поэтому перед переводом на версию M3 3 необходимо в качестве первой итерации перевести все подобные механизмы на objectpack.
Типичная задача, решаемая этим компонентом - зависимые гриды или деревья. Например, слева отображается таблица (или дерево) и по выбору элементов в этой таблице - в другой таблице, например, справа данные будут обновляться.
MD-представление теперь реализуется двумя паками - для master-грида(дерева) и для detail-грида соответственно.
Master-pack имеет вид:
class ArticlePack(objectpack.ObjectPack): model = Article # list_window указывать не нужно, т.к. экшн этим занимается сам def __init__(self): super(ArticlePack, self).__init__() self.replace_action( 'list_window_action', ArticleListWindowAction())
Экшн, отвечающий за отображение MD-окна имеет вид:
class GarageMDWindowAction(objectpack.actions.MasterDetailWindowAction): # Можно указать класс окна - потомка от MasterDetailWindow #window_clz = ui.ArticleListWindow @property def detail_pack(self): # возвращается экземпляр пака для detail-грида return ControllerCache.find(CommentPack) # здесь можно модифицировать окно перед его рендерингом, # однако значительные изменения лучше производить в окне-наследнике def create_window(self): super(GarageMDWindowAction, self).create_window() self.win.title = self.parent.title
Типичная задача, решаемая наследованием типового окна - переопределение класса master grid. Например, в том случае, когда мастером должно быть дерево:
class ArticleListWindow(objectpack.ui.MasterDetailWindow): master_grid_clz = ExtObjectTree
В основном изменения затронули механизм рендеринга UI.
Основные отличия:
__slots__
;eval
в пользу Ext.create
Ключевое отличие - это отказ от django-шаблонизатора
в пользу сериализация в json-представление. Фактически с версии m3 3 ui-представление должно возвращаться с сервера в виде
json, которое в последствии на клиенте сериализуется и передается как параметр в функции Ext.create
.
Таким образом не используется eval
, что существенно облегчает отладку.
UI можно описывать как декларативно - через python-словарь, так и в старом стиле через описание классов.
Пример использования декларативного стиля:
win = { 'xtype': 'm3-window', 'height': 500, 'width': 400, 'items': [ 'xtype': 'form', ] }
Аналогично:
win = ExtWindow( height=500, width=400, items=[ ExtForm() ] )
Или:
win = ExtWindow() win.height = 500 win.width = 400 win.items.append(ExtForm())
Более подробную информацию по каждому компоненту и его работе можете найти в примерах использования.
До версии m3 3 функцию генерации UI выполнял django-шаблонизатор, который на каждый запрос отдавал js-представление.
Оно впоследствии eval
-лилось на клиенте с помощью функции smart_eval
.
Это выглядело примерно так:
var form = Ext.getCmp("{{component.form.client_id}}"); var tab_panel = Ext.getCmp("{{component.tab_panel.client_id}}"); var other_props = Ext.getCmp("{{component.other_props.client_id}}"); var inn2 = Ext.getCmp("{{component.inn2.client_id}}"); var kpp2 = Ext.getCmp("{{component.kpp2.client_id}}"); var name_other_org = Ext.getCmp("{{component.name_other_org.client_id}}"); win.on('beforesubmit',function(){ for (i=1;i<tab_panel.items.length;i++){ tab_panel.activate(i); } }); function fill_bankprop_fields(args){ Ext.getCmp("{{component.bank1.client_id}}").setValue(args.namep); Ext.getCmp("{{component.bik.client_id}}").setValue(args.newnum); Ext.getCmp("{{component.bank_filial.client_id}}").setValue(''); Ext.getCmp("{{component.correspondent_account.client_id}}").setValue(args.ksnp); Ext.getCmp("{{component.short_address.client_id}}").setValue(args.nnp); } function selectBankTemplate(){ if (form) { var baseParams = {}; Ext.Ajax.request({ url: '{{ component.select_bank_url }}', params: Ext.applyIf(baseParams || {}, win.actionContextJson || {}), success: function(response, opts){ smart_eval(response.responseText); }, failure: uiAjaxFailMessage }); } return false; } other_props.on("check" ,setReadOnlyFunc); setReadOnlyFunc(other_props); function setReadOnlyFunc(opt){ if(opt.getValue()){ inn2.setReadOnly(false); kpp2.setReadOnly(false); name_other_org.setReadOnly(false); }else{ inn2.setReadOnly(true); kpp2.setReadOnly(true); name_other_org.setReadOnly(true); } } win.on("fill_fields", function(args){ //проверяем перед заменой, заполнены ли поля var not_empty_str = Ext.getCmp("{{component.bank1.client_id}}").value + Ext.getCmp("{{component.bik.client_id}}").value + Ext.getCmp("{{component.bank_filial.client_id}}").value + Ext.getCmp("{{component.correspondent_account.client_id}}").value + Ext.getCmp("{{component.short_address.client_id}}").value; if(not_empty_str.length > 0) { Ext.Msg.show({ title: 'Перезапись данных', msg: 'Некоторые поля банковских реквизитов уже заполнены.<br />Произвести перезапись?', buttons: Ext.Msg.YESNO, fn: function(btn, text){ if (btn == 'yes'){ fill_bankprop_fields(args); } }, icon: Ext.Msg.WARNING }); } else { fill_bankprop_fields(args); } });
component.form.client_id
- возвращается идентификатор, по которому в дальнейшем производится поиск компонента
в отрендеренном пространстве браузера;component.select_bank_url
- так передается url для последующего Ajax-запроса.Как стало сейчас:
Ext.define('Ext.paidserv.BankPropsDictAddWindow', { extend: 'Ext.m3.EditWindow', xtype: 'bank-props-dict-add-window', initComponent: function () { this.callParent(); this.tab_panel = this.findByItemId('tab_panel'); this.other_props = this.findByItemId('other_props'); this.inn2 = this.findByItemId('inn2'); this.kpp2 = this.findByItemId('kpp2'); this.name_other_org = this.findByItemId('name_other_org'); this.bank1 = this.findByItemId('bank1'); this.bik = this.findByItemId('bik'); this.bank_filial = this.findByItemId('bank_filial'); this.correspondent_account = this.findByItemId('correspondent_account'); this.short_address = this.findByItemId('short_address'); this.on('beforesubmit', function () { var i = 0; for (i = 1; i < this.tab_panel.items.length; i++) { this.tab_panel.activate(i); } }, this); this.other_props.on("check", this.setReadOnlyFunc, this); this.on('afterrender', this.setReadOnlyFunc, this); this.bank1.on('beforerequest', function(cmp, req){ if (req) { req.success = function (win) { win.on('select', this.onSelectBank, this); return win; }.bind(this) } }, this); this.bank1.on('change', this.onChangeBank, this); }, onSelectBank: function(cmp, id, displayText){ // достанем запись о банке var bank_rec = cmp.grid.getSelectionModel().getSelected(); this.bank1.setRecord(bank_rec); }, onChangeBank: function(){ var bank_rec = this.bank1.getRecord(); if (bank_rec) { this.bik.setValue(bank_rec.json['newnum']); this.bank_filial.setValue(''); this.correspondent_account.setValue(bank_rec.json['ksnp']); this.short_address.setValue(bank_rec.json['nnp']); } }, setReadOnlyFunc: function () { if (this.other_props.getValue()) { this.inn2.setReadOnly(false); this.kpp2.setReadOnly(false); this.name_other_org.setReadOnly(false); } else { this.inn2.setReadOnly(true); this.kpp2.setReadOnly(true); this.name_other_org.setReadOnly(true); } }, bind: function(data){ this.callParent(arguments); } });
По аналогии с xtype, каждый плагин должен зарегестрировать ptype. Пример использования ptype в python-коде:
# Было self.charge_grid.plugins.append('new Ext.ux.grid.GridSummary()') # Стало self.charge_grid.plugins.append({'ptype': 'gridsummary'})
js-представление, должно находиться в static-файлах так же должно иметь xtype. Для этого в settings.py в кортеж
STATICFILES_FINDERS должен быть добавлен следующий элемент - m3.finders.RecursiveAppDirectoriesFinder
-
это позволит иметь папку static в любой вложенности приложения
Получение ссылки на вложенный компонент производится через вызов метода окна - findByItemId, поиск ведется по атрибуту itemId. Этот параметр можно задавать из python-кода, по-умолчанию подставляется название атрибута, например:
class MyWindow(ExtWindow): _xtype = 'my-window' def __init__(self): self.form = ExtForm() self.items.append(self.form)
Такая конструкция равносильна python-коду
class MyWindow(ExtWindow): _xtype = 'my-window' def __init__(self): self.items.append(ExtForm(item_id='form'))
Это эквивалентно json-представлению вида:
{ "xtype": "my-window", "items": [ "xtype": "form", "itemId": "form" ] }
Все функции превратились в методы класса Ext.paidserv.BankPropsDictAddWindow
С версии m3 3 в javascript появилась возможность использовать механизм обещаний из библиотеки q.js. Примеры работы и документацию можно найти на офф. сайте.
UI.evalResult
- Сериализует в json и обрабатывает полученное значение, создавая экземпляр компонентаUI.ajax
- Загружает JSON AJAX-запросом и кладёт в promise, пример использования:
// Пример отправки запроса на сервер и отображение окна, если в качестве результата возвращается окно UI.ajax({ url: data.url, params: data.context }).then(UI.evalResult) .catch(uiAjaxFailMessage);
UI.callAction
- Производит вызов ajax-запроса, обработку его и возвращает promise. Внутри себя производит все
необходимые действия по отображению окна, работе с масками, модальностью. Примеры:
// Пример отправки запроса на сервер с параметрами, получения json-результата и обработка результата UI.callAction.call(this, { url: this.loadSelectedQueryUrl, method: 'POST', params: { query_id: selected_query }, success: function(obj){ this.query_str.setValue(obj['query']); }.createDelegate(this), failure: uiAjaxFailMessage });
В случае успеха будет установлено значение в поле this.query_str
.
// Пример отправки запроса на сервер, получение json-конфигурации окна и подписка на событие UI.callAction.call(this, { url: this.grid_edit_url, params: params, success: function (win) { win.on('addRow', function (data) { this.saveRec(data); }, this); }.bind(this) });
В случае успеха происходит подписание экземпляра окна на событие addRow совместно с реализацией обработчика.
UI.require
- подгрузка модулей и их зависимостей. Пример использования:
// Пример отправки запроса на сервер, сириализация ответа и подтягивание зависимостей // перед созданием и отображением окна UI.ajax({ method: 'POST', url: this.rule_edit_url, params: Ext.apply(baseParams, this.getContext() || {}) }).then(UI.evalResult) .then(function(result) { if (result.config) { // Подтягиваем зависимости UI.require([result.config['xtype']]) .spread(function () { return [result.config, result.data]; }).spread(UI.createWindow); } }) .catch(uiAjaxFailMessage);
Атрибут класса ExtGridColumn summaryType раньше хранил значения вида, пример:
{ 'header': u'Сумма', 'data_index': 'summa', 'sortable': True, 'align': 'right', 'renderer': 'FloatRenderer', 'width': 20, 'extra': {'summaryType': '"sum"'}, },
sum, сейчас необходимость использовать "строку в строке" пропала, нужно использовать sum как значение, например:
{ 'header': u'Сумма', 'data_index': 'summa', 'sortable': True, 'align': 'right', 'renderer': 'FloatRenderer', 'width': 20, 'extra': {'summaryType': 'sum'}, },
Все функции-хенлдеры или рендереры будут искаться в sсope окна как обработчики:
self.grid = ExtObjectGrid( region='center', sm=ExtGridCheckBoxSelModel()) self.grid.add_column( header=u'№', width=10, renderer="AutoRenderer") self.grid.add_column( header=u'Состояние', data_index='state_verbose', width=40, sortable=True) self.grid.add_column( header=u'Дата документа', data_index='docdate', width=40, sortable=True) self.grid.add_column( header=u'Дата операции', data_index='operdate', width=40, sortable=True) self.grid.add_column( header=u'Номер', data_index='docnum', width=21, sortable=True) self.grid.add_column( header=u'Позиции', data_index='count', width=24, sortable=True) self.grid.add_column( header=u'Сумма', data_index='summa', width=25, align="right", sortable=True, renderer="FloatRenderer") self.grid.add_column( header=u'Тип', data_index='type_verbose', width=60, sortable=True) self.grid.add_column( header=u'Комментарий', data_index='comment', width=110, sortable=True) # кнопки в верхней панели грида self.grid.top_bar.items.append(ExtButton( text=u'Зарегистрировать', icon_cls='x-form-file-icon', handler='enableRegistration'))
Необходимо, чтобы функция AutoRenderer, FloatRenderer и enableRegistration были в ExtJS-классе окна, например:
Ext.define('Ext.paidserv.CorrDocsArchiveWindow', { extend: 'Ext.m3.Window', xtype: 'corrdocs-archive-window', AutoRenderer: AutoRenderer, // ссылка на глобальную функцию FloatRenderer: FloatRenderer, StateRenderer: StateRenderer, bind: function (data) { this.callParent(arguments); if (data.readOnly) { this.setBlocked(data.readOnly, data.readOnlyExclude); this.grid.getTopToolbar().getComponent('button_edit').setText('Просмотр'); } this.setTitle(this.title + data.title); this.date_since.setValue(data.dateSince); this.date_until.setValue(data.dateUntil); this.register_submit_url = data.register_submit_url; this.debtWindowUrl = data['debtWindowUrl']; }, ... enableRegistration: function () { var actionType = true; this.registerAction(actionType); }, registerAction: function (actionType) { //получаем выделенные строки var selRecords = this.grid.getSelectionModel().getSelections(), selectedId = [], i, maskText = actionType == true ? 'Регистрация документа' : 'Снятие регистрации документа'; for (i = 0; i < selRecords.length; i++) { selectedId[i] = selRecords[i].id; } UI.callAction.call(this, { loadMaskText: maskText, url: this.register_submit_url, params: Ext.applyIf({'ids': selectedId.join(','), "action_type": actionType}, this.getContext()), success: function (response, opts) { this.grid.getSelectionModel().clearSelections(); this.grid.store.load(); }.bind(this), failure: uiAjaxFailMessage }); } });
Однако функция summaryRenderer не может быть задана в качестве строкового представления в python-коде, такую функцию необходимо устанавливать в extjs-представлении, например:
setSummaryRenderer: function (grid, name, func) { var cm, col; cm = grid.getColumnModel(); col = cm.findColumnIndex(name); cm.config[col].summaryRenderer = func; } setSummaryRenderer(this.grid, 'state_verbose', function () { return 'Итого по документам:' });
Все вызовы Ext.Ajax.request, которые внутри себя вызывали smart_eval должны быть переписаны в концепции с функции UI.callAction
Удалены классы ExtJsonReader и ExtDataReader, вместо использования этих классов необходимо все параметры передавать в ExtStore, например:
# Было self.reader = ExtJsonReader(total_property='total', root='rows') self.reader.set_fields( 'id', 'identical', 'tab_num', 'fio', 'consumer_id', 'service_ref_name', 'service_id', 'bank_props_name', 'bank_props_id', 'servicepoint_ref_name', 'servicepoint_id', 'summa') self.grid.store = ExtGroupingStore(auto_load=True, root='rows', id_property='id') self.grid.store.reader = self.reader # Стало self.grid.store = ExtGroupingStore( auto_load=True, root='rows', id_property='id', total_property='total', root='rows', fields=[ 'id', 'identical', 'tab_num', 'fio', 'consumer_id', 'service_ref_name', 'service_id', 'bank_props_name', 'bank_props_id', 'servicepoint_ref_name', 'servicepoint_id', 'summa'] )
Атрибут triiger_action_all в класс ExtDictSelectField был переименован в trigger_action, пример использования:
# Было self.servicepoint_field = fields.ExtDictSelectField( anchor='90%', name='servicepoint', display_field='name', value_field="id", label=u'Группа', trigger_action_all=False, # False = query, True = all ask_before_deleting=False, hide_trigger=False, hide_edit_trigger=True, width=190) # Стало self.servicepoint_field = fields.ExtDictSelectField( anchor='90%', name='servicepoint', display_field='name', value_field="id", label=u'Группа', trigger_action=ExtDictSelectField.QUERY, ask_before_deleting=False, hide_trigger=False, hide_edit_trigger=True, width=190)
Было переименовано событие closed_ok в select, когда происходит выбор элемента в окне справочника.
Возьмем для примера задачу, которая отрисовывает окно. Причем окно наследуется от продуктового базового класса, которое имеет определенную логику на javascript. Чтобы было более ясно, приложены diff-ки с отличиями. Слева пример версии m3 3, справа пример версии m3 2.
Отличия реализации UI
submit_url
- используется в методе bind
, файла ~/static/js/base-report-window.js (далее)Отличия класса-наследника для UI BaseReportWindow
Отличия файлов логики:
Файл логики на javascript для версии m3 2 - ~/templates/ui-js/BaseReportWindow.js:
var win = Ext.getCmp("{{component.client_id}}"); var form = Ext.getCmp("{{component.form.client_id}}"); var sp_grid = Ext.getCmp("{{component.sp_grid.client_id}}"); var knd_grid = Ext.getCmp("{{component.knd_grid.client_id}}"); var rayon_grid = Ext.getCmp("{{component.rayon_grid.client_id}}"); var period_since = Ext.getCmp("{{component.period_since.client_id}}"); var period_until = Ext.getCmp("{{component.period_until.client_id}}"); var print_all_sps = Ext.getCmp("{{component.print_all_sps.client_id}}"); var print_state_serv = Ext.getCmp("{{component.print_state_serv.client_id}}"); var print_paid_serv = Ext.getCmp("{{component.print_paid_serv.client_id}}"); var print_serv_detail = Ext.getCmp("{{component.print_serv_detail.client_id}}"); var print_ent_detail = Ext.getCmp("{{component.print_ent_detail.client_id}}"); var service = Ext.getCmp("{{component.service_field.client_id}}"); function collectGridIds(grid){ var selIds = []; var selRecords = grid.getSelectionModel().getSelections(); for (var j=0;j<selRecords.length;j++){ selIds.push(selRecords[j].id); } return Ext.util.JSON.encode(selIds); } if (print_all_sps){ print_all_sps.on("check", function(){ if (print_all_sps.getValue()){ if (sp_grid) sp_grid.getSelectionModel().selectAll(); } else { if (sp_grid) sp_grid.getSelectionModel().clearSelections(); } }); } if (sp_grid){ sp_grid.getSelectionModel().on('selectionchange',function(){ if (knd_grid) knd_grid.getStore().load(); }); } if (knd_grid){ knd_grid.getStore().on('beforeload', function(request){ request.baseParams["sps"] = collectGridIds(sp_grid); }); } //метод получения всех значений формы function getSubmitValues(form){ if (!form) return {}; if (knd_grid && !knd_grid.getSelectionModel().hasSelection()) { Ext.Msg.show({ title: 'Внимание!', msg: 'Не выбрано ни одного ребенка для печати!', icon: Ext.MessageBox.WARNING, buttons: Ext.MessageBox.OK }); return false; } if (rayon_grid && !rayon_grid.getSelectionModel().hasSelection()) { Ext.Msg.show({ title: 'Внимание!', msg: 'Не выбрано ни одного района для печати!', icon: Ext.MessageBox.WARNING, buttons: Ext.MessageBox.OK }); return false; } var params = {}; if (period_since) params['period_since'] = period_since.getValue(); if (period_until) params['period_until'] = period_until.getValue(); if (print_serv_detail) params['print_serv_detail'] = print_serv_detail.getValue(); if (print_ent_detail) params['print_ent_detail'] = print_ent_detail.getValue(); if (print_paid_serv) params['print_paid_serv'] = print_paid_serv.getValue(); if (print_state_serv) params['print_state_serv'] = print_state_serv.getValue(); if (print_all_sps) params['print_all_sps'] = print_all_sps.getValue(); if (sp_grid) params["sps"] = collectGridIds(sp_grid); if (knd_grid) params["knds"] = collectGridIds(knd_grid); if (rayon_grid) params["rayons"] = collectGridIds(rayon_grid); if (service) params["service"] = service.getValue(); return params; } function okHandler(btn, arguments){ var formParams = getSubmitValues(form); if (!formParams) return; var mask = new Ext.LoadMask(win.body, {msg:'Подготовка формы...'}); var params = Ext.apply(win.actionContextJson, formParams || {}); mask.show(); btn.disable(); Ext.Ajax.request({ url:"{{component.form.url}}", params: params, success: function(response, opts){ mask.hide(); smart_eval(response.responseText); win.close(); }, failure: function(response, opts){ uiAjaxFailMessage.apply(win, arguments); }, timeout: 600000 //10минут }); } function closeHandler(btn, arguments){ win.close(); }
Файл логики на javascript для версии m3 3 - ~/static/js/base-report-window.js
Ext.define('Ext.paidserv.BaseReportWindow', { extend: 'Ext.m3.Window', xtype: 'base-report-window', initComponent: function () { this.callParent(); this.form = this.findByItemId('form'); this.sp_grid = this.findByItemId('sp_grid'); this.knd_grid = this.findByItemId('knd_grid'); this.rayon_grid = this.findByItemId('rayon_grid'); this.period_since = this.findByItemId('period_since'); this.period_until = this.findByItemId('period_until'); this.print_all_sps = this.findByItemId('print_all_sps'); this.print_state_serv = this.findByItemId('print_state_serv'); this.print_paid_serv = this.findByItemId('print_paid_serv'); this.print_serv_detail = this.findByItemId('print_serv_detail'); this.print_ent_detail = this.findByItemId('print_ent_detail'); this.service = this.findByItemId('service'); if (this.print_all_sps) { this.print_all_sps.on("check", function () { if (this.print_all_sps.getValue()) { if (this.sp_grid) { this.sp_grid.getSelectionModel().selectAll(); } } else { if (this.sp_grid) { this.sp_grid.getSelectionModel().clearSelections(); } } }.bind(this)); } if (this.sp_grid) { this.sp_grid.getSelectionModel().on('selectionchange', function () { if (this.knd_grid) { this.knd_grid.getStore().load(); } }, this); } if (this.knd_grid) { this.knd_grid.getStore().on('beforeload', function (request) { request.baseParams["sps"] = this.collectGridIds(this.sp_grid); }, this); } }, collectGridIds: function (grid) { var selIds = [], selRecords = grid.getSelectionModel().getSelections(); for (var j = 0; j < selRecords.length; j++) { selIds.push(selRecords[j].id); } return Ext.encode(selIds); }, //метод получения всех значений формы getSubmitValues: function (form) { if (!form) return {}; if (this.knd_grid && !this.knd_grid.getSelectionModel().hasSelection()) { Ext.Msg.show({ title: 'Внимание!', msg: 'Не выбрано ни одного ребенка для печати!', icon: Ext.MessageBox.WARNING, buttons: Ext.MessageBox.OK }); return false; } if (this.rayon_grid && !this.rayon_grid.getSelectionModel().hasSelection()) { Ext.Msg.show({ title: 'Внимание!', msg: 'Не выбрано ни одного района для печати!', icon: Ext.MessageBox.WARNING, buttons: Ext.MessageBox.OK }); return false; } var params = {}; if (this.period_since) { params['period_since'] = this.period_since.getValue(); } if (this.period_until) { params['period_until'] = this.period_until.getValue(); } if (this.print_serv_detail) { params['print_serv_detail'] = this.print_serv_detail.getValue(); } if (this.print_ent_detail) { params['print_ent_detail'] = this.print_ent_detail.getValue(); } if (this.print_paid_serv) { params['print_paid_serv'] = this.print_paid_serv.getValue(); } if (this.print_state_serv) { params['print_state_serv'] = this.print_state_serv.getValue(); } if (this.print_all_sps) { params['print_all_sps'] = this.print_all_sps.getValue(); } if (this.sp_grid) { params["sps"] = this.collectGridIds(this.sp_grid); } if (this.knd_grid) { params["knds"] = this.collectGridIds(this.knd_grid); } if (this.rayon_grid) { params["rayons"] = this.collectGridIds(this.rayon_grid); } if (this.service) { params["service"] = this.service.getValue(); } return params; }, okHandler: function (btn, arguments) { var formParams = this.getSubmitValues(this.form); if (!formParams) { return; } UI.callAction.call(this, { method: 'POST', url: this.submitUrl, params: Ext.apply(formParams || {}, this.getContext()), success: this.close.createDelegate(this), failure: uiAjaxFailMessage, timeout: 600000 // 10 минут }); }, bind: function (data) { this.submitUrl = data['submit_url']; } });
Как видим отличия как правило заключаются в качественно другом представлении javascript-кода.
Доступны множество разнообразных примеров на версии m3 3:
Чтобы посмотреть работу примеров необходимо сделать несколько шагов:
Создать рабочее окружение для демо
$ workon m3-tests
Склонировать m3-blank
(m3-tests)$ hg clone https://bitbucket.org/barsgroup/m3-blank
Установить обновленные версии objectpack, m3-ext, m3-core
(m3-tests)$ pip install m3-core m3-ext objectpack
Предварительно позаботившись, чтобы была установлена переменная окружения
PIP_INDEX_URL=http://pypi.bars-open.ru/simple/
Добавить в INSTALLED_APPS
демо-приложения, например:
INSTALLED_APPS = ( 'django.contrib.staticfiles', 'm3', 'm3_ext', 'm3_ext.ui', 'm3_ext.demo', 'objectpack', 'objectpack.demo', )
Запустить демо-сервер:
(m3-tests)$ python manage.py runserver
Должен запуститься рабочий стол с заполненным пунктом меню и ярлыками на рабочем столе.