⬆️ ⬇️

Multi-page SPA on Python

Bridge between Python and React



An owl is a nano-framework that can be embedded into other frameworks.

A picture from sova.online, which runs 3 http servers:

http://sova.online/ - just Falcon

http://sova.online:8000/ - just Django

http://sova.online:8001/ - just Python (login: 1, password: 1)

There are source codes and installation instructions. There is no advertising.







The idea to make sites on Python with drawing on React is not new. There is a wonderful framework https://plot.ly/products/dash/ , why do something else?

I explain: Owl is not designed for site development. It is a tool for replacing thick clients with browser-based applications (desktop applications).

')

- What is a web client?

- Not. This is not a web client. This application works in a browser.

- I do not understand.

- Unfortunately, many developers do not understand.



As a general, I actively worked with several Internet applications.



Online client of the bank Ugra (bank closed).

It was a good application, but it was a Java applet, i.e. fat client launched from browser. And the Bank of Ugra, and applets in the past.



Online client of VTB-24 Bank (bank closed).

I am a humanist, but after working with this miracle, cruel thoughts like: “Make the developer register 1000 payment orders in it” began to appear.

At the same time as a web client, he is beautiful. Animation, opens on a mobile phone. Wow Cool!

I asked an accountant friend: how do you work with him?

She says: beautiful! I load the data in 1s, I work in 1s, I upload the results back.



Sberbank online client

Satisfactory customer, you can work. When I was asked to rate him, I gave him 3 points out of 5 and gave a list of comments. This is with my 10 payments per month. Those who make 100 bills a day are likely to upload information.



Filling payment.



The green area that occupies 20% of the screen is the menu. It does not just interfere (position: fixed), it says that the developer is not a professional. If I started making a payment, on the db screen. 3 buttons: “Create”, “Save as template”, “Cancel”. These buttons are (for some reason below). This is not a multi-page SPA: go through the menu item - the data in the form will disappear.

The one who did this will not even understand what the hitting is: “It’s normally done, everyone is doing it, such a library, people are working ...”. And he is right. You need to ask the project manager, and for the manager, the main thing is the database and the layers in the conceptual model. And the forms - we will hire boys, they will draw. And they are proud, perhaps, of this hack.



Trading platforms (5 pieces, 44 FZ)

These are really applications (not web clients). But I don't like field controllers.

Examples:





Aligned strangely, the width of the field is clearly insufficient, there is no autoheight in the input field.



Another example. In the field “Date of publication” there is no template dd.mm.yyyy, the calendar with an error, the calendar icon scares:





List on rts-tender: there is a current record highlighted in color, you can move through the list with arrows, but there is no autoscrolling (you can run off the screen), neither Enter nor a space opens the link, the tab is not tied to the current record. Although you can only open the link with the mouse, I appreciate the control with a plus sign. This functionality (remember and highlight the current document) is not enough for me in mail.ru



It seems like little things. But a professional application differs from a semi-professional in just trifles. The end user does not care what your database is and how many layers there are in the conceptual model. It works with screen forms and it has 3 requirements: functionally, conveniently, quickly.

Unfortunately, the choice of the system is determined by IT specialists and managers who did not work with the system themselves and will not work. They will appreciate the speed, the functionality will be assessed as they understand it, and they do not care about convenience, the main thing is that it is beautiful.

Pavel Valerievich Durov did not invent either the social network or the instant messenger. He did what users want, comfortable and beautiful. And people appreciated it, including materially.



Owl is a tool for building a professional interface.

What does this mean, for example, SED.



There are EDS, it has 3 user groups:

Bosses

Specialists preparing documents

Clerks.



Chiefs, they are like children. They need to just and beautiful. Ideally, 1 button and 1 field. And to ponte more. Web client and, of course, mobile client to show off.



Specialists. Web client, a social network / mailer mix. Do not forget that there are a lot of specialists and they need to be trained. The more familiar the environment is to them, the better. A mobile client is also useful if the security service allows it.



Clerks. This is where the Owl comes in handy. Clerks are a backbone user group. All others can get sick / go on vacation / stop using, - SED will work. If registration stops, everything will come up.

Office work is a conveyor, and here everything is important, any trifle: fonts, half tones, automatic filling, checking values, ease of entry, etc.

SED "Delo". Performers' cabinets are made on the web client, the office is a fat client. Everything is fine, but it will work until the government bans Windows in state structures. I love Win 7, but if I were the ruler, the it-market would cheer up with new orders, and MS would remain in bright memory. By the way, on December 6, Anton Siluanov signed a directive on switching to domestic software.



Sova.online



How owl opens the form.

Without multi-page.

The central element of the Owl is the Document component. By pressing ctrl-U on the start page, you will see everything you need to create a Document class object:

- data fields of the database;

- form url to display;

- dbAlias, unid - to work with the database;

- something else there.



To some extent, Document is the equivalent of a Redux form.

The form is loaded as a JSON string, then the former dictionary becomes an object having style, className and an array (list) of elements. The array will be inserted into the element with id = root as
<div style className>……</div> 


Array elements are objects that describe tags.
 <div>, <a>, <img>, <button> 
or array, or components.

For parsing an array, the boxing function is responsible. If an element containing an array is encountered, it will recursively call itself.

Navel of the earth, of course, div.

In the simplest case, this is the string: dict (div = 'Hello', className = 'h2')

But maybe an array (array of arrays):

 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()] }, ]), ] ) 




Here are 3 panels (each in a separate file: subFormTop.py, etc.).

subFormTop.panel () returns an array to build the top panel.

subFormLeft.panel () and subFormRight.panel () are combined into a row ('className': 'row') and describe the left and right panels.

subFormDown.panel () is commented out (not useful).



It may seem difficult. But this is Python: everything can be simplified.

An example of a form from the magazine "Reports". The labField function (label, field_name_BD) returns an array of two dictionaries (table row): 1st dictionary is {'div': label}, the second {'field': [database_name, '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(), ] 








Examples from sova / api / forms / home / top.py (starting at sova.online):



python dictionary

{'a': 'React v16', 'href': 'https://reactjs.org'}

Gives clear React component

 <a href={'https://reactjs.org'}>React v16</a> 




Img is smarter than the standard - in props you can set href and target:



Python:

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



Fragment of the parser that converts an array of objects into components (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; } 




Type in the search engine "react component library". The result is predictable - a lot. But all this abundance is intended for websites, not applications:

Smart-textarea - perhaps the only control that gave me.

React-select - simplified and redid the dropdown list

Data-picker / calendar - did not find anything suitable. I wrote my own, taking as a model built into G.Chrome.

Upload / Download - nothing suitable, wrote your own.



Imho: websites have a sad future. The vast majority of users will no longer use browsers (or have already ceased) in the near future. The phone will be merged with the tablet, and 1 dash of 10 applications will fully meet your needs.

I have already come across twice with programmers who do not know how to write an e-mail address correctly. Why should they remember what they do not use. The world is changing.



In the Sovet, the controllers are not ideal, but they were designed for the operator, not the web user.

As an example, the “Transmission Mark” form. A fairly universal form, used where there is a boss. Red on the screenshot encircled fields that control the hide. Additional resolutions open automatically as they are completed, if there are several instructions in the operative part for different groups of performers with different terms. Two terms per group: 1st term to the 1st performer, 2nd term co-performers.





You can touch the form HERE



The controller is the React component associated with the database field.

A detailed description of the controllers with the ability to check their work is on Sova.online.

Pay attention to the types of rtf and json. Rtf is displayed as text, but if the text contains the {_ {object} _} construct, the Owl will perform json.parse for this construct and add the result to the form. The field with the json type must store the description of the array of markup elements: [{element1}, {element2}, ...]. json.parse is executed before rendering.

Fields with such types allow you to store markup in the database or in files. Useful when generating reports and writing documentation.



List of controllers for all field types (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}/>; }; }; 




The application requires a manipulation mechanism for the controllers.

In the board, all document controllers are stored in a document variable.

this.register

I did not dare to use refs due to rumors that the redoxophiles would cancel it.

The controller can have the following interfaces:

getValue (param)

setValue (value, param)

setFocus ()

changeDropList ()



In order to access the desired field, there are document methods

getField (fieldName, param)

setField (fieldName, value, param)

changeDropList (fieldName, param)

setFocus (fieldName)

For fields of type FileShow there is a method fileShow ['FILES1 _']. HasAtt (), where FILES1_ is the name of the file area. Returns true if there are attachments. In the mark of the transfer of such areas 2.



Controllers can generate a recalc event. If a handler is registered for this field, it will be executed. Handlers are in loadable js-files.

An example and a somewhat simplified description:

There is a “Transmission Mark” form (o.py). It contains the uploaded o.js file

Handlers are registered in o.js

 recalc: { PROJECTO: doc => doc.forceUpdate(), WHOPRJ2: doc => doc.forceUpdate(), WHOPRJ3: doc => doc.forceUpdate(), …   } 


, as well as the conditions for hiding are specified (project, op, prj1, prj2… prj5 - this is the “name” property in the description of the divs):

 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')), }, 




How it works: the PROJECTO field is the checkbox; when the value changes, the controller generates a recalc event, the document calls the recalc.PROJECTO (this) handler.

The handler simply calls forceUpdate () to redraw the document.

When redrawing, it is checked whether the components have a name in the name, whether the hide [props.name] function has a name for this, and whether it returns true.

prj3: doc =>! doc.getField ('projectO') || (! doc.getField ('whoPrj2') &&! doc.getField ('whoPrj3'))

Hide the third resolution (area with props.name === 'prj3'), if the projectO checkbox is OFF or the resolution 2 and 3 fields are not entered (both 'whoPrj2' and 'whoPrj3' fields are empty).

The name of a field when calling functions is case-insensitive.

WHOPRJ2 is a combo box, when choosing a value, the controller will also generate a recalc event, which will also cause a redraw. Selecting the artist in the second resolution, you will open the third one.



In loadable js-files you can:

- manage the hide;

- manage reading only;

- respond to changes in fields;

- execute commands of buttons;

- do validation of fields and forms before saving;



Loadable file for form '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' }, }, } 




Validation of fields is a function, returns empty if everything is OK, or a message about what is wrong. The owl will set the focus to an invalid field.

Form validation - promis. In the example, there are no checks (yes is always called), just something is done before being sent to the server.

In redux-form, validation is done through trow - some kind of wildness.



For those who are not familiar with promises, an example of the simplest:

 const confirmDlg = msg => new Promise((ok, cancel) => confirm(msg) ? ok('  ') : cancel('  cancel')); confirmDlg('  ') .then( s => console.log(s)) .catch( s => console.log(s)); 




The Document class has several predefined commands that can be used in buttons:



edit: go to form edit mode

save: save the form

close: close form

saveClose: save and close the form



prn: print the form with the choice of template for printing

docOpen: open the document

dbOpen: open log

xopen: open url

newDoc: create a new document with the desired form



In the Redux-form api is richer, - in the Owl, only necessary.



Multi-page.



The Document class creates an object (form) that is embedded in the element.
 <div id="root"></div> 
.

Let's call it the “root document”. If you add an element to the root document

<div style = {{position: 'absolute', zIndex: 999}} />, in the same way you can insert another Document object.

How to deal with loadable command handlers? Everything is simple: each form has its own handler (its js), and the root document should load those that may be required.

Sample for the start page of sova.online (home.py)

The home.py form opens documents with the forms “rkckg”, “outlet”, “outlet.gru”, and “o” to demonstrate its multiplicity.

In order for all forms to work correctly, you need to write scripts for these forms in 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'] 




Since when calling any handler function, the first parameter is a link to the document, actions will be performed on the required document.



OOP and no miracles.



React - not React

I have already described the form "report". It opens from the report manager (“React” arrow) and describes the parameters for collecting the report.



The reports themselves (arrow "not React") are stored in subordinate documents with the form "rreport" in the form of html attachments. We were engaged in the development of reports, when React was not, the form “rreport” turned out to be simple (20 html lines and 15 simple-js lines), why change the fact that it has been working for 8 years.



Open report manager.



The “rreport” form consists of 4 buttons and an iframe. Owl before opening the document replaces src = "" in the address with the url line for downloading the html-attachment, the browser does the rest.

The EXCEL / WORD buttons are similar: insert the url buttons for downloading with the file name “report.html.xls” or “report.html.doc” and the corresponding mime-type. Excel / Word does the rest (“these smart animals understand everything they want from them”).

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




When opening html in Excel / Word, there are differences from the browser, but small. And the article is not about that.



Making a form from scratch.



Initial data:

There are 3 functions

def snd (* msg, cat = 'snd'):

def err (* msg, cat = 'all'):

def dbg (* msg, cat = 'snd'):

which are more or less evenly distributed throughout the code and write error messages and other crap to the log file.

The message format is passed to logging.Formatter in the form:

'% (asctime) s% (levelname) s [% (name) s]% (message) s'

The file is filled with messages

...

09/02/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] Attachments are saved in ". \ DB \ files" Free 68557 Mb

09/02/2018 17:58:07 ERROR [do_get.py] getScript: [Errno 2] No such file or directory: 'sova / api / forms / o / oo.js'

...

Date-time, then level, then in square brackets the category within the level, then the message.



Task:

Make a page to view the log file. What type





Let's call the form “lm”, it will be formed as a function of the page in the module 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)') ), ] ) 




In the left part there are 2 fields, both with the type “list”: type and cat (message type and category).

In the right one field msg with type fd (forDisplayOnly).

The message types are specified in the field description (['All log | all', 'Errors | err', ...),

categories are drawn on xhr from the global dictionary with a tricky url call:

api.get? loadDropList & logger | keys_err returns in json-format an array (list) of categories from the global dictionary. Something like well ('logger', 'keys_err').

Messages are generated when the document is opened by the queryOpen function in lm.py

 def queryOpen(d, mode, ground): logParser() ls = well('logger_all', 'AL L') s = '\n'.join(reversed(ls)) d.msg = s d.type_alias = 'all' 


logParser reads and parses the log file. The results are decomposed into several arrays and stored in the global dictionary. Nothing interesting: 2 simplest re and loop iterator.

Functions for working with the global dictionary:

toWell (o, key1, [key2]) - save the object “o” in the global dictionary

well (key1, [key2]) - take an object from the global dictionary by key (by two keys).

For the first drawing of this is enough. To be able to display messages of the desired type and the desired category, you need to make loadable js.

Add the line to lm.py

javaScriptUrl = 'jsv? api / forms / lm / lm.js'

and create 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 + '|AL 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 pulls messages from the server of the desired type and category:

 def getLogData(par, un): lg, _, cat = par.partition('|') msg = well('logger_' + lg, cat) return 200, 'text/html; charset=UTF-8', '\n'.join(reversed(msg)) 




You can enjoy the form HERE .

Initially, logging was done based on the standard logging module.

using logging.FileHandler, .addHandler, and others getLogger and setFormatter.

As taught. But at the same time it was buggy. You can throw stones, but when I threw out logging and just started writing to the file, the code became shorter, clearer and the glitches disappeared.



Included is a self-hosted multi-threaded wsgi server with Digest authorization. This is not for sites. Why was he needed at all?

The customer has 40 jur. Persons, in most cases 1-2-3 people work with the system. Store data on the Internet is prohibited. All win 7. Requires easy installation and configuration.

Solution: using cx-Freeze and Inno Setup, we install the installer, run it on the computer of the most responsible one and get a mini http server for the local network that starts as a Windows service. Nothing extra. It is impossible to use wsgiref.simple_server or wsgi_Werkzeug built into Python. they are single-threaded: until one request is completed, the others will wait.

I would hardly surprise anyone by saying that the built-in WSGIServer / 0.2 CPython / 3.5.3 in Django is many times faster than the self-written Python one. Only it doesn’t matter - forms and directories are cached on the client, only the database data is transmitted very quickly over the local network.

There is another reason: the desktop application has access to computer resources (EDS, files, scanner ...). To get the same access from the browser, you need to either write a plugin, or hang up a small http server in the services, which can sniff out the main server and perform the necessary actions on the locale.



Owl does not use framework tools to work with the database. In the dbToolkit directory, something similar in structure to MongoDB (or Lotus Notes) to SQLite3:

Book class - db (in MongoDB and Lotus Notes terminology)

DocumentCollection class - a collection of documents from the Book

Document class - a document (an object containing any number of fields).



Installation:

Download from sova.online owl.zip archive



In the archive directory owl, from which you can run the Owl from django, falcon or without frameworks.



Download, unzip.

Install Python3 (3.5+)



1. owl - without frameworks. Attention! Login: 1, password: 1



Linux:

cd ./owl

python3 wsgi_sova.py



or in a separate window

screen -Udm python3 wsgi_server.py



Windows:

cd ./owl

wsgi_sova.py



2. Django



Linux:

Install django:

pip3 install django

cd ./owl

python3 manage.py runserver



or in a separate window

screen -Udm python3 manage.py runserver 127.0.0.1:8000



Windows:

Install 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



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



- the name of the article is strange, did you understand what a “Multipage SPA” is?

- the usual marketing ploy

- why not Redux? Everyone uses Redux

- the word "reducer" I do not like

- and seriously? CombineReducers at any level of the hierarchy ... It's so beautiful

- it's multipage, baby. Command handlers should be inside the form, not like a deer antler.

- why did you even write an article?

- PR

Source: https://habr.com/ru/post/424779/



All Articles