Мост между Python и React

Сова – это нано-фреймворк, который можно встроить в другие фреймворки.
Картинка с sova.online, на котором запущено 3 http сервера:
http://sova.online/ — просто Falcon
http://sova.online:8000/ — просто Django
http://sova.online:8001/ — просто Python (логин: 1, пароль: 1)
Там же исходные коды и инструкция по установке. Там же нет рекламы.



Идея делать сайты на Питоне с прорисовкой на React не нова. Есть замечательный фреймворк https://plot.ly/products/dash/, зачем еще что-то делать?
Объясняю: Сова не рассчитана на разработку сайтов. Это инструмент для замены толстых клиентов на приложения, работающие через браузер (десктопные приложения).

— Что-ли веб-клиент?
— Нет. Это не веб-клиент. Это приложение, работающее в браузере.
— Не понимаю.
— К сожалению, и многие разработчики не понимают.

Я как генеральный активно работал с несколькими интернет приложениями.

Онлайн клиент банка Югра (банк закрыт).
Хорошее было приложение, но это был Java applet, т.е. толстый клиент, запускаемый из браузера. И банк Югра, и апплеты в прошлом.

Онлайн клиент банка ВТБ-24 (банк закрыт).
Я гуманист, но поработав с этим чудом, стали появляться жестокие мысли типа: «Заставить разработчика зарегистрировать в нем 1000 платежек».
При этом как веб-клиент он прекрасен. Анимация, открывается на мобильнике. Вау! Круто!
Спросил знакомого бухгалтера: как ты с ним работаешь?
Она говорит: прекрасно! загружаю данные в 1с, в 1с работаю, результаты выгружаю обратно.

Онлайн клиент сбербанка
Удовлетворительный клиент, работать можно. Когда меня попросили его оценить, я поставил ему 3 бала из 5 и дал список замечаний. Это при моих 10 платежках в месяц. Те, кто делает 100 платежек в день, скорее всего, выгружают информацию.

Заполнение платежки.

Зеленая область, занимающая 20% экрана – это меню. Оно не просто мешает (position: fixed), оно говорит о том, что разработчик не профессионал. Если я начал создавать платеж, на экране д.б. 3 кнопки: «Создать», «Сохранить как шаблон», «Отмена». Эти кнопки есть (почему-то внизу). Здесь не multi-page SPA: перейдешь по пункту меню, — данные в форме пропадут.
Тот, кто это делал, даже не поймет, в чем наезд: «Нормально сделано, все так делают, такая библиотека, люди ведь работают …». И он прав. Спрашивать надо с руководителя проекта, а для руководителя главное – это БД и слои в концептуальной модели. А формы – наймем мальчиков, они нарисуют. И ведь гордятся, наверное, этой халтурой.

Торговые площадки (5 штук, 44 ФЗ)
Это действительно приложения (не веб-клиенты). Но контроллеры полей мне не нравятся.
Примеры:


Выровнено странно, ширина поля явно недостаточна, autoheight в поле ввода отсутствует.

Еще пример. В поле «Дата публикации» нет шаблона дд.мм.гггг, календарь с ошибкой, пиктограмма календаря пугает:


Список на rts-tender: есть выделенная цветом текущая запись, стрелками можно двигаться по списку, но нет автоскроллинга (можно убежать за границу экрана), ни Enter, ни пробел не открывают ссылку, табулятор не привязан к текущей записи. Хотя открыть ссылку можно только мышкой, я оцениваю контрол со знаком плюс. Такого функционала (запомнить и подсветить текущий документ) мне не хватает в mail.ru

Вроде как мелочи. Но профессиональное приложение отличается от полупрофессионального именно мелочами. Конечному пользователю наплевать, какая у вас БД и сколько слоев в концептуальной модели. Он работает с экранными формами и у него 3 требования: функционально, удобно, быстро.
К сожалению, выбор системы определяют айтишники и начальники, которые сами с системой не работали и работать не будут. Скорость они оценят, функциональность оценят так, как они это понимают, а на удобство им наплевать, главное, чтобы было красиво.
Павел Валерьевич Дуров не изобретал ни соцсеть, ни мессенджер. Он сделал то, что нужно пользователям, удобно и красиво. И люди это оценили, в том числе и материально.

Сова – это инструмент для построения профессионального интерфейса.
Что это значит, на примере СЭД.

Есть СЭД, в ней 3 группы пользователей:
Начальство
Специалисты, готовящие документы
Делопроизводители.

Начальники, они как дети. Им нужно, чтобы просто и красиво. В идеале 1 кнопка и 1 поле. И чтобы понтов побольше. Веб-клиент и, конечно, мобильный клиент, чтобы понты демонстрировать.

Специалисты. Веб-клиент, помесь соцсеть/почтовик. Не забывайте, что специалистов много и их надо обучать. Чем привычней будет для них среда, тем лучше. Мобильный клиент тоже пригодится, если служба безопасности разрешит.

Делопроизводители. Вот здесь пригодится Сова. Делопроизводители – это системообразующая группа пользователей. Все остальные могут заболеть/уйти в отпуск/перестать пользоваться, — СЭД будет работать. Если остановится регистрация, встанет все.
Делопроизводство – это конвейер, и здесь важно все, любая мелочь: шрифты, полутона, автоматическое заполнение, проверка значений, удобство ввода и т.д.
СЭД «Дело». Кабинеты исполнителей сделаны на веб-клиенте, канцелярия – толстый клиент. Все прекрасно, но это будет работать, пока правительство не запретит Windows в госструктурах. Я люблю Win 7, но если бы правителем был я, it-рынок взбодрился новыми заказами, а MS остался в светлой памяти. Кстати, 6 декабря Антон Силуанов подписал директиву о переходе на отечественное ПО.

Sova.online

Как Сова открывает форму.
Без multi-page.
Центральным элементом Совы является компонент Document. Нажав ctrl-U на стартовой странице, вы увидите все, что нужно для создания объекта класса Document:
— данные полей БД;
— url формы для отображения;
— dbAlias, unid — для работы с БД;
— что-то там еще.

В некоторой степени Document – это аналог Redux-form.
Форма загружается в виде JSON строки, затем бывший словарь становится объектом, имеющим style, className и массив (список) элементов. Массив будет вставлен в элемент с id=root в виде
<div style className>…массив…</div>

Элемента массива — это объекты, описывающие теги
<div>, <a>, <img>, <button>
, или массив, или компоненты.
За парсинг массива отвечает функция boxing. Если встретится элемент, содержащий массив, она рекурсивно вызовет себя.
Пуп земли, конечно, div.
В простейшем случае это строка: dict(div='Привет', className='h2')
Но может быть и массив (массив массивов):
def style(**par):
    return {'style': {**par}}

dict( # словарь описывает стартовую страницу sova.online
    style(position='relative'),
    readOnly = 1,
    div = [
        dict( style(width=1000, margin='auto', paddingTop=20),
            div=[
                { 'div': subFormTop.panel() },
                { 'div': [subFormLeft.panel(), subFormRight.panel()], 'className': 'row' },
#                 { 'div': [subFormDown.panel()] },
        ]),
    ]
)


Здесь 3 панели (каждая в отдельном файле: subFormTop.py и т.д.).
subFormTop.panel() возвращает массив для построения верхней панели.
subFormLeft.panel() и subFormRight.panel() объединены в строку ('className': 'row') и описывают левую и правую панели.
subFormDown.panel() закомментирована (не пригодилась).

Возможно, покажется сложным. Но это Питон: все можно упростить.
Пример формы, из журнала «Отчеты». Функция labField(метка, имя_поля_БД) возвращает массив из двух словарей (строку таблицы): 1-й словарь – это {'div': метка}, второй {'field': [имя_поля_БД, 'tx']}.
div = [

    docTitle('Отчет'),

    dict ( wl='40mm', className='cellbg-green', div=_table(
        labField('Отчет', 'nodafd'),
        labField('Запуск отчета', '_STARTINGTIME'),
        labField('Окончание', '_ENDTIME'),
        labField('Пользователь', 'CREATOR'),
        
        labField('Категория', 'REPORTCAT'),
        labField('Название', 'REPORTNAME'),
        labField('Заголовок', 'REPORTTITLE'),
        labField('Начало периода', 'dt1'),
        labField('Конец периода', 'dt2'),
        labField('Начало периода 2', 'dt3'),
        labField('Конец периода 2', 'dt4'),
        labField('Журналы', 'LBYEARS'),
        labField('Префиксы', 'GRGROUP'),
        labField('Формула отбора', 'QUERYMAIN'),
        labField('Комментарий', 'NOTES'),
    )),

    sent(),
]




Примеры из sova/api/forms/home/top.py (стартовая на sova.online):

питоновский словарь
{'a': 'React v16', 'href': 'https://reactjs.org'}
Породит понятную React компоненту
<a href={'https://reactjs.org'}>React v16</a>


Img поумнее стандартного – в пропсах можно задать href и target:

Питон:
dict(img='image?react.ico', style={'width':16}, href='https://reactjs.org')

Фрагмент парсера, преобразующего массив объектов в компоненты (boxing.js):
if ( td.img ) { // td – это элемент массива
	let img = <img src={td.img} прочие_пропсы/>;
	return td.href ?
		<a href={td.href} key={i} target={td.target}>{img}</a>
		:
		img;
}


Наберите в поисковике «react component library». Результат предсказуем, — очень много. Но все это изобилие предназначено веб-сайтам, не приложениям:
Smart-textarea – пожалуй, единственный контрол, который меня устроил.
React-select –упростил и переделал выпадающий список
Data-picker/calendar – не нашел ничего подходящего. Написал свой, взяв за образец встроенный в G.Chrome.
Upload / Download – ничего подходящего, написал свой.

Imho: у веб-сайтов грустное будущее. Подавляющее большинство пользователей в ближайшем будущем перестанет пользоваться браузерами (или уже перестало). Телефон срастется с планшетом, а 1 тире 10 приложений полностью обеспечат потребности.
Я уже дважды сталкивался с программистами, которые не знают, как правильно написать адрес e-mail. Зачем им помнить то, чем они не пользуются. Мир меняется.

В Сове контроллеры не идеальные, но они разрабатывались в расчете на оператора, а не веб-пользователя.
Как пример форма «Отметка о передаче». Достаточно универсальная форма, используется там, где есть начальник. Красным на скриншоте обведены поля, управляющие скрытием. Дополнительные резолюции открываются автоматически по мере заполнения, если в резолютивной части несколько поручений разным группам исполнителям с разными сроками. Два срока на группу: 1-й срок 1-му исполнителю, 2-й срок соисполнителям.


Потрогать форму можно ЗДЕСЬ

Контроллер – это компонента React связанная с полем БД.
Подробное описание контроллеров с возможностью проверить их работу есть на Sova.online.
Обратите внимание на типы rtf и json. Rtf отображается как текст, но, если в тексте встретится конструкция {_ {объект} _}, Сова выполнит для этой конструкции json.parse и добавит результат в форму. Поле с типом json должно хранить описание массива элементов разметки: [{элем1}, {элем2}, …]. json.parse выполняется перед рендерингом.
Поля с такими типами позволяют хранить разметку в БД или в файлах. Полезно при формировании отчетов и написании документации.

Список контроллеров для всех типов полей (controllers.js):
export const controller = prop => {
	switch (prop.type) { //тип поля
		case 'chb':	return <Checkbox {...prop}/>;
		case 'lbse': // listbox single enable (разрешить значения не из списка)
		case 'lbme': // listbox multivalue enable
// lbse/lbme - это текстовое поле с кнопкой, открывающей список

		case 'tx':	return <Text {...prop}/>; // smart-textarea
		case 'lbsd':  // listbox single disables (запретить значения не из списка)
		case 'lbmd':	return <ListBox {...prop}/>;
		case 'dt':	return (prop.readOnly ?
					<Text {...prop} xValue={Util.dtRus(prop.xValue)} />
					:
					<Datepicker {...prop}/>);
		case 'fd':	return <ForDisplayOnly {...prop}/>;
		case 'table':
		case 'gr':	return <Table {...prop}/>;
		case 'rtf':	return <RTF {...prop}/>;
		case 'json':	return <JsonArea {...prop}/>;
		case 'list':	return <List {...prop}/>;
		case 'view':	return <View {...prop}/>;
		default:
			console.warn('Неизвестный тип поля', prop.xName, prop.type);
			return <Text {...prop}/>;
	};
};


Для работы приложения необходим механизм манипуляции контроллерами.
В сове все контроллеры документа хранятся в переменной документа
this.register
Использовать refs я не рискнул из-за слухов, что редаксофилы его отменят.
Контроллер может иметь следующие интерфейсы:
getValue(param)
setValue(value, param)
setFocus()
changeDropList()

Для того, чтобы обратиться к нужному полю, есть методы документа
getField(fieldName, param)
setField(fieldName, value, param)
changeDropList(fieldName, param)
setFocus(fieldName)
Для полей типа FileShow есть метод fileShow['FILES1_'].hasAtt(), где FILES1_ имя области с файлами. Возвращает true, если есть вложения. В отметке о передаче таких областей 2.

Контроллеры могут генерировать событие «recalc». Если для данного поля прописан обработчик, он выполнится. Обработчики находятся в подгружаемых js-файлах.
Пример и несколько упрощенное описание:
Есть форма «Отметка о передаче» (o.py). В ней прописан подгружаемый файл o.js
В o.js прописаны обработчики
recalc: {
    PROJECTO: doc => doc.forceUpdate(),
    WHOPRJ2: doc => doc.forceUpdate(),
    WHOPRJ3: doc => doc.forceUpdate(),
    … другие обработчики
}

, а также прописаны условия скрытия (project, op, prj1, prj2…prj5 – это свойство «name» в описании div'ов):
hide: {
        project: doc => !doc.getField('projectO'), // скрыть, если поле PROJECTO пустое
        op: doc => doc.getField('projectO'), // скрыть, если поле PROJECTO не пустое
        prj1: doc => !doc.getField('projectO'),
        prj2: doc => !doc.getField('projectO'),
        prj3: doc => !doc.getField('projectO') || (!doc.getField('whoPrj2') && !doc.getField('whoPrj3')),
        prj4: doc => !doc.getField('projectO') || (!doc.getField('whoPrj3') && !doc.getField('whoPrj4')),
        prj5: doc => !doc.getField('projectO') || (!doc.getField('whoPrj4') && !doc.getField('whoPrj5')),
      },


Как это работает: поле PROJECTO – это чекбокс, при изменении значения контроллер генерирует событие recalc, документ вызывает обработчик recalc.PROJECTO(this).
Обработчик просто вызывает forceUpdate() – перерисовать документ.
При перерисовке проверяется, есть ли у компоненты в пропсах name, есть ли для этого name функция hide[props.name] и не вернет ли она true.
prj3: doc => !doc.getField('projectO') || (!doc.getField('whoPrj2') && !doc.getField('whoPrj3'))
Скрывать третью резолюцию (область с props.name === 'prj3'), если чекбокс 'projectO' в состоянии OFF или в полях резолюций 2 и 3 исполнители не введены (оба поля 'whoPrj2' и 'whoPrj3' пустые).
Имя поля при вызове функций регистронезависимо.
WHOPRJ2 – это поле со списком, при выборе значения контроллер также сгенерит событие recalc, которое также вызовет перерисовку. Выбрав исполнителя во второй резолюции, вы тем самым откроете третью.

В подгружаемых js-файлах можно:
— управлять скрытием;
— управлять только чтением;
— реагировать на изменения полей;
— выполнять команды кнопок;
— делать валидацию полей и формы перед сохранением;

Подгружаемый файл для формы 'fo':

window.sovaActions = window.sovaActions || {}; 
window.sovaActions.fo = { // fo – имя формы на нижнем регистре

    recalc: { // обработчик вызывается при изменении значений чекбоксов и полей со списком
        PROJECTO: doc => doc.forceUpdate(),
    },

    hide: {   // скрывать именованную область, если true
        project: doc => !doc.getField('projectO'),
    },

    readOnly: {   // только чтение для именованной области, если true
        who: doc => doc.getField('SENTFROMDB'),
    },

    validate: { // проверка полей и формы перед отправкой на сервер
        who: doc => doc.getField('who') ? '' : 'Не заполнено поле "Кому направлено"',

        form: doc => new Promise( (yes, no) => {
            let disableAutoOrder = false;
            for (let i = 1; i <= 5; i++) {
                let val = doc.getField('RESPRJ' + i);
                disableAutoOrder  |= /за моей подписью/.test(val);
            }
            disableAutoOrder && doc.setField('AUTOORDER', '');
            yes();
        }),
    },

    cmd: { // выполнение команд кнопок
        logoff: doc => { window.location.href = '/logoff' },
    },

}


Валидация полей – функция, возвращает пусто, если все ОК, или сообщение о том, что не так. Сова установит фокус на невалидное поле.
Валидация формы – промис. В примере проверок нет (всегда вызывается yes), просто что-то выполняется перед отправкой на сервер.
В redux-form валидация сделана через trow – дикость какая-то.

Для тех, кто не знаком с промисами, пример простейшего:
const confirmDlg = msg => new Promise((ok, cancel) => confirm(msg) ? ok('Нажата кнопка ОК') : cancel('Нажата кнопка cancel'));
confirmDlg('Самый простой промис')
    .then( s => console.log(s))
    .catch( s => console.log(s));


Класс Document имеет несколько предопределенных команд, которые можно использовать в кнопках:

edit: перейти в режим редактирования формы
save: сохранить форму
close: закрыть форму
saveClose: сохранить и закрыть форму

prn: распечатать форму с выбором шаблона для печати
docOpen: открыть документ
dbOpen: открыть журнал
xopen: открыть url
newDoc: создать новый документ с нужной формой

В Redux-form api побогаче, — в Сове только необходимое.

Multi-page.

Класс Document создает объект (форму), который встраивается в элемент
<div id="root"></div>
.
Назовем его «корневой документ». Если в корневой документ добавить элемент
<div style={{position: 'absolute', zIndex: 999}}/>, в него точно также можно вставить другой объект Document.
Как быть с подгружаемыми обработчиками команд? Все просто: каждая форма имеет свой обработчик (свой js), и корневой документ должен загрузить те из них, которые могут потребоваться.
Пример для стартовой страницы sova.online (home.py)
Форма home.py, чтобы продемонстрировать многостраничность, открывает документы с формами «rkckg», «outlet», «outlet.gru», «o».
Чтобы все формы корректно работали, необходимо прописать в home.py скрипты для этих форм:
javaScriptUrl = ['jsv?api/forms/rkckg/rkckg.js',
                 'jsv?api/forms/outlet_gru/outlet_gru.js',
                 'jsv?api/forms/outlet/outlet.js',
                 'jsv?api/forms/o/o.js',
                 'jsv?api/forms/home/home.js']


Поскольку при вызове любой функции обработчика первым параметром передается ссылка на документ, действия будут выполнены с нужным документом.

ООП и никаких чудес.

React – не React
Я уже описывал форму «report». Она открывается из менеджера отчетов (стрелка «React») и описывает параметры сбора отчета.

Сами отчеты (стрелка «не React») хранятся в подчиненных документах с формой «rreport» в виде html вложений. Мы занимались разработкой отчетов, когда React не было, форма «rreport» получилась простая (20 строк html и 15 строк просто-js), зачем менять то, что 8 лет работает.

Открыть менеджер отчетов.

Форма «rreport» состоит из 4-х кнопок и iframe. Сова перед открытием документа заменяет в айфрейме src="" на строку с url для скачивания html-вложения, остальное делает браузер.
С кнопками EXCEL / WORD аналогично: вставляем в нужное место кнопки url для скачивания с именем файла «report.html.xls» или «report.html.doc» и соответствующим mime-type. Остальное делают Excel/Word («эти умные животные отлично понимают все, что от них хотят» ).
Из do_get.py:

downloadUrl = '/download/' + fn + '?' + '&'.join([d.db.alias, d.unid, idbl, fsName, fzip, ctype, flen])
excel = '/download/%s.xls?%s' % (fn, '&'.join([d.db.alias, d.unid, idbl, fsName, fzip, 'application/x-excel', flen])) 
word = '/download/%s.doc?%s' % (fn, '&'.join([d.db.alias, d.unid, idbl, fsName, fzip, 'application/msword', flen]))

html = html.replace('src=""', 'src="%s"' % downloadUrl).replace('openExcel', excel).replace('openWord', word)


При открытии html в Excel/Word отличия от браузера есть, но небольшие. Да и статья не об этом.

Делаем форму с нуля.

Исходные данные:
Есть 3 функции
def snd(*msg, cat='snd'):
def err(*msg, cat='all'):
def dbg(*msg, cat='snd'):
, которые более-менее равномерно распределены по всему коду и пишут в лог-файл сообщения об ошибках и прочую хрень.
Формат сообщений передается в logging.Formatter в виде:
'%(asctime)s %(levelname)s [%(name)s] %(message)s'
Файл заполняется сообщениями

02.09.2018 17:50:07 DEBUG [http-server] addr ('127.0.0.1', 49964), «GET /arm HTTP/1.1» 200 –
02.09.2018 17:54:07 INFO [Free space] Вложения сохраняются в ".\DB\files" Свободно 68557 Mb
02.09.2018 17:58:07 ERROR [do_get.py] getScript: [Errno 2] No such file or directory: 'sova/api/forms/o/oo.js'

Дата-время, затем уровень, затем в квадратных скобках категория внутри уровня, затем сообщение.

Задача:
Сделать страницу для просмотра лог-файла. Что типа


Назовем форму «lm», формироваться она будет функцией page в модуле api/forms/lm.py
def page(dbAlias, mode, userName, multiPage):
    
    return dict(
        style(background='url(/image?bg51.jpg)', backgroundSize='100% 100%'),
        div=[
            dict(
                style(width='200px', float='left', background='rgba(210,218,203, 0.5)', padding='0 5px'),
                div=[
                    _field('type', 'list', ['Весь журнал|all', 'Ошибки|err', 'Сообщения|info', 'Отладка|debug'],
                           saveAlias=1,
                           **style(margin='10px auto', width=170, height=110)
                    ),
    
                    _field('cat', 'list', 'TYPE_ALIAS|||api.get?loadDropList&logger|keys_{FIELD}',
                           listItemClassName='repName',
                           listItemSelClassName='repNameSel',
                           **style(height='calc(100vh - 133px)', overflow='auto')
                    )
                ],
            ),
    
            _field('msg', 'fd',
                   br=1,
                   **style(overflow='auto', height='100vh', font='bold 12px Courier', background='rgba(255,255,255, 0.8)')
            ),
        ]
    )


В левой части 2 поля, оба с типом «list»: type и cat (тип сообщения и категория).
В правой одно поле msg с типом fd (forDisplayOnly).
Типы сообщений прописаны в описании поля (['Весь журнал|all', 'Ошибки|err',...),
категории вытягиваются по xhr из глобального словаря с вызовом хитрого url:
api.get?loadDropList&logger|keys_err вернет в json-формате массив (список) категорий из глобального словаря. Что-то типа well('logger', 'keys_err').
Сообщения формируются при открытии документа функцией queryOpen в lm.py
def queryOpen(d, mode, ground):
    logParser()
    ls = well('logger_all', 'A L L')
    s = '\n'.join(reversed(ls))
    d.msg = s
    d.type_alias = 'all'

logParser считывает и парсит log-файл. Результаты раскладывает в несколько массивов и сохраняет их в глобальном словаре. Ничего интересного: 2 простейших re и цикл по итератору.
Функции для работы с глобальным словарем:
toWell(o, key1, [key2]) — сохранить в глобальном словаре объект «o»
well(key1, [key2]) — взять из глобального словаря объект по ключу (по двум ключам).
Для первой прорисовки этого достаточно. Чтобы можно было отобразить сообщения нужного типа и нужной категории, необходимо сделать подгружаемый js.
В lm.py добавляем строку
javaScriptUrl = 'jsv?api/forms/lm/lm.js'
и создаем lm.js:
window.sovaActions = window.sovaActions || {};
window.sovaActions.lm = { // обработчики формы "lm"
    init: doc => doc.changeDropList('CAT'),
    recalc: {
        TYPE: (doc, label, alias) => {
            doc.changeDropList('CAT');
            getLogData(doc, alias + '|A L L');
        },
        CAT: (doc, label) => getLogData(doc, doc.getField('type_alias') + '|' + label),
    },
};

// *** *** ***

let getLogData = (doc, keys) => {
    fetch('api.get?getLogData&' + keys, {method: 'get', credentials: 'include'})
        .then( response => response.text() )
        .then( txt => doc.setField('msg', txt) )
        .catch( err => doc.setField('msg', err.message) );
};


getLogData вытягивает с сервера сообщения нужного типа и нужной категории:
def getLogData(par, un):
    lg, _, cat = par.partition('|')
    msg = well('logger_' + lg, cat)
    return 200, 'text/html; charset=UTF-8', '\n'.join(reversed(msg))


Насладиться формой можно ЗДЕСЬ.
Изначально логгирование было сделано на основе стандартного модуля logging
с использованием logging.FileHandler, .addHandler и прочих getLogger и setFormatter.
Как учили. Но при этом глючило. Можете кидаться камнями, но когда я выкинул logging и стал просто писать в файл, код стал короче, понятней и глюки исчезли.

В комплекте есть самописный многопоточный wsgi сервер с Digest авторизацией. Это не для сайтов. Зачем он вообще понадобился?
У заказчика 40 юр. лиц, в большинстве случаев с системой работают 1-2-3 человека. Хранить данные в интернете запрещено. У всех win 7. Требуется простота установки и конфигурации.
Решение: с помощью cx-Freeze и Inno Setup делаем инсталлятор, запускаем его на компьютере самого ответственного и получаем мини-http сервер для локальной сети, стартующий как сервис Windows. Ничего лишнего. Использовать встроенный в Питон wsgiref.simple_server или wsgi_Werkzeug нельзя, т.к. они однопоточные: пока один запрос не отработает, другие будут ждать.
Вряд ли я кого удивлю, сообщив, что встроенный в Django WSGIServer/0.2 CPython/3.5.3 в разы быстрее самописного питоновского. Только это не имеет значения, — формы и справочники кэшируются на клиенте, по локальной сети очень быстро передаются только данные БД.
Есть еще одна причина: десктопное приложение имеет доступ к ресурсам компьютера (ЭЦП, файлы, сканер...). Чтобы из браузера получить аналогичный доступ надо или писать плагин, или повесить в сервисах маленький http-сервер, который может и с главным сервером снюхаться, и нужные действия на локале выполнит.

Сова не использует инструменты фреймворков для работы с БД. В директории dbToolkit нечто похожее по структуре на MongoDB (или на Lotus Notes) на SQLite3:
Класс Book – db (в терминологии MongoDB и Lotus Notes)
Класс DocumentCollection – коллекция документов из Book
Класс Document – документ (объект, содержащий любое количество полей).

Установка:
Скачать с sova.online архив owl.zip

В архиве каталог owl, из которого можно запустить Сову из django, falcon или без фреймворков.

Скачать, распаковать.
Установить Python3 (3.5+)

1. owl – без фреймворков. Внимание! Логин: 1, пароль: 1

Linux:
cd ./owl
python3 wsgi_sova.py

или в отдельном окне
screen -Udm python3 wsgi_server.py

Windows:
cd ./owl
wsgi_sova.py

2. Django

Linux:
Установить django:
pip3 install django
cd ./owl
python3 manage.py runserver

или в отдельном окне
screen -Udm python3 manage.py runserver 127.0.0.1:8000

Windows:
Установить django:
pip install django
cd ./owl
manage.py runserver

3. falcon

Linux:
pip3 install falcon
cd ./owl
python3 wsgi_sova.py falconApp:api 8001 log_falcon/falcon

Windows:
pip install falcon
cd ./owl
wsgi_sova.py falconApp:api 8001 log_falcon/falcon

*********************

— название у статьи странное, сам то понял, что такое «Multipage SPA»?
— обычный маркетинговый ход
— а почему без Redux? Все используют Redux
— слово «редюсер» мне не нравится
— а серьезно? СombineReducers на любом уровне иерархии… Это так красиво
— это multipage, детка. Обработчики команд должны быть внутри формы, а не как рога у оленя
— зачем ты вообще статью написал?
— пиарюсь

Комментарии (10)


  1. keydon2
    18.12.2018 01:41

    А можно не делать из моего браузера толстый клиент или веб-приложение, чтобы оно если и не летало, то хотя бы не тормозило?


    1. aon24 Автор
      19.12.2018 14:10

      Убрал задержки 0.4с на каждое окно (было сделано, чтобы акцентировать открытие).
      Убрал мягкую прорисовку (opacity: '1', transition: 'opacity 0.4s ease'… было сделано, чтобы окно не выскакивало сразу, а как бы проступало. Для глаза почти не заметно, но бережет зрение.


  1. sshmakov
    18.12.2018 05:18

    По моему впечатлению бухгалтеру не надо ходить в онлайн банк вообще, поскольку существует бесшовная и бесплатная интеграция сервисов банка с 1С, называемая DirectBank.


    1. aon24 Автор
      19.12.2018 14:25

      Я главный бухгалтер и у меня нет 1с.
      И статья не об этом.


      1. sshmakov
        19.12.2018 14:27

        Угу. Извините, что влез.


  1. Temmokan
    18.12.2018 06:28

    Только сайт, возможно, местами кривоват. Нажимаю «О проекте» в Firefox последнего выпуска, и вижу:

    image

    (ниже не пролистывается).


    1. aon24 Автор
      19.12.2018 14:43
      +1

      Спасибо, подправил


  1. excentro
    18.12.2018 07:02

    Очень интересно, спасибо. Надеюсь станет аналогом vaadin на python :)


    1. aon24 Автор
      20.12.2018 10:53

      vaadin что-то мутит. 8 не совместима с 10. Не уверен, что CUBA на 10 работает.
      И vaadin и CUBA платные.


  1. stilet69
    19.12.2018 21:56

    Впечатлен… Реально впечатлен…