ผลต่างระหว่างรุ่นของ "Multi-component Page"
(ไม่แสดง 60 รุ่นระหว่างกลางโดยผู้ใช้คนเดียวกัน) | |||
บรรทัดที่ 7: | บรรทัดที่ 7: | ||
* '''Consolidate populating request''' for all forms and tables on the page. | * '''Consolidate populating request''' for all forms and tables on the page. | ||
* String together '''page edit sequence'''. | * String together '''page edit sequence'''. | ||
== Page Sequence == | |||
In CuneiFox, modifying a multi-component entry (usually a document) is done in pre-defined steps. This design is to make the process more predictable and, thus, minimize the complexity of data handling behind the scene. The most common pattern of a multi-component page behaves as follows: | |||
# The page is opened first in read-mode. In this mode, modifications to any form fields and table data are disabled. | |||
# To make any modification, the user must enter the edit-mode, where forms and tables are enabled on a step-by-step basis. Entering the edit-mode by clicking the page-level Add or Edit button first allows the user to modify elements in the first page sequence (highlighted yellow). | |||
# The user can proceed to the next sequence by clicking the page-level Proceed button. | |||
# The user can may exit back into the read-mode by: | |||
#* Clicking the page-level Cancel button at any step. | |||
#* Clicking the page-level Proceed button on the last editing step. | |||
'''NOTE:''' Step 3 and 4 can be blocked until certain conditions are met via Document Context key '''[[#Page Permission & Sequence Definition|proceed_lock_seq]]'''. | |||
[[File:Edit mode example.png|720px|thumb|center|alt=Example page in read-mode and edit-mode.|Example page in read-mode and edit-mode.]] | |||
=== Page-level Permission === | |||
When dealing with a multi-component record, one cannot assume the permission bit of that record type dictate the actions actually allowed for each sub-component. For example, a user authorized to edit ''(page-level permission: 3)'' Accounting Journal is expected to be able to delete ''(table-level permission: 4)'' individual debit and credit records as well. | |||
Therefore, it is perfectly acceptable to max out the element-level permissions of all tables and forms on the page ''(set to 4 or higher)'', as long as the page-level permission exists to control users' access to different operation. On the server-side, it is also recommended to recheck whether the operation requested from the client-side is allowed. ''(If the developer follows the common pattern, this check is taken care of via the function '''{{code|process_allowed_action}}'''.) | |||
=== Beacon & Document Locking === | |||
CuneiFox's beacon & document locking system prevents multiple users from modifying the same document simultaneously. In essence, the document locking works as follows: | |||
# '''Locking a document''' is done by updating the column '''{{code|editting}}''' ''(the misspelling has become intentional, a relic of a past mistake)'' of the document's master entry (usually the document header) with '''the username'''. ''(See [[#Page Permission → Document Lock → Allowed Actions]] below.)'' | |||
# '''Unlock the document''' by updating the '''{{code|editting}}''' column with a blank string ({{code|lang=python|''}}). The document unlock happens when: | |||
#* The user exits from the edit-mode on an existing document. (A newly created master entry counts as an existing document.) | |||
#* The document is unlocked temporarily when the browser tab is hidden while the user is in the edit-mode. The document is automatically re-locked when the tab returns to visibility. | |||
==== Beacon Keys ==== | |||
For the beacon system to properly lock/unlock a document, it must know the document is in the company's database. Beacon keys help point the system to the correct target. | |||
* '''Javascript-packed:''' These set of keys indicate the client-side state when the beacon is sent, and thus must be packaged along with the beacon via client-side Javascript. For pages using the usual patterm, these keys are automatically packaged and are accessible on the server-side via either {{code|lang=python|request.data}} or {{code|lang=python|request.form['bdata']}}. | |||
** '''token''' ''(str)'': The session token. This is used to determine whether the session's user can lock/unlock the document. ''(For example, a user cannot unlock a document that is locked by another user.)'' | |||
** '''mode''' ''(str)'': The type of the beacon sent. This value can take either of the following: | |||
*** {{code|lang=javascript|'start'}}: The user first enters the edit-mode. If the document already exists, CuneiFox will lock the document. | |||
*** {{code|lang=javascript|'resume'}}: The page resumes edit-mode after automatically exiting it as the user navigates to other tabs or apps. If the document exists, CuneiFox will lock the document. | |||
*** {{code|lang=javascript|'done'}}: The user exits the edit-mode via the page-level Proceed button while being in the final page sequence. CuneiFox unlocks the document. | |||
*** {{code|lang=javascript|'cancel'}}: The user exits the edit-mode via the page-level Cancel button. If the document exists, CuneiFox will unlock the document. | |||
** '''cur_seq''' ''(int)'': The number representing the page sequence the user is in when the beacon fires (with the first sequence denoted by {{code|lang=javascript|0}}). This value is not utilized during the lock/unlock step, but can be highly necessary during the document's wrapup algorithms. | |||
** '''seq_count''' ''(int)'': The number of steps in the page sequence. | |||
** '''cur_mainid''' ''(int)'': The ID of the document's main record (the document's header). | |||
** '''time_diff''' ''(float)'': The difference between the server time and the session time in seconds. Because of the way CuneiFox stores its data, this value is necessary to pinpoint the database file in need. | |||
* '''Database Pointer:''' The variables that points the program to the correct database file. This is usually ''hard-coded into each specific beacon-handling route'' or ''fed via request parameters for a shared route''. | |||
** '''bookcode''' ''(str)'': The name of the database file. In CuneiFox convention, this is usually a 3-lettered string. | |||
** '''is_gl''' ''(str: {{code|lang=python|'yes'}} or {{code|lang=python|'no'}})'': Whether the target database is an Accounting Journal. ''(By CuneiFox convention, an accounting journal database name is prefixed with {{code|GL_}} for example {{code|GL_GL0}}.)'' | |||
** '''datatype''' ''(str: {{code|lang=python|'table'}}, {{code|lang=python|'data'}}, or {{code|lang=python|'report'}})'': The data type (location) of the database file. | |||
* '''Other Keys:''' These keys are quite rare and are usually explicitly hard-coded in each specific Python route when needed. | |||
** '''skip_lock''' ''(bool)'': Used exclusively by the Python function '''{{code|lang=python|process_doclock}}''' to make it return immediately without performing document locking/unlocking. | |||
==== Beacon Routine Functions ==== | |||
The 2 groups of beacon keys discussed above can be converted into a dict form for ease of use via the Python function '''{{code|lang=python|process_beacon_args}}'''. | |||
'''<syntaxhighlight lang="python" id="process_beacon_args"> | |||
process_beacon_args(internal_args, request) | |||
</syntaxhighlight>''' | |||
{| style="margin-left:20px;" | |||
|- style="vertical-align:top;" | |||
| style="width:120px;" | '''Parameters''' || | |||
* '''internal_args''' ''(dict or {{code|lang=python|None}})'': Provides beacon keys-values in cases of beacon-like internal operations. When this value is not {{code|lang=python|None}}, the beacon aruguments are only read from this argument. | |||
* '''request''' ''(Flask request)'': The client-side request. If no '''internal_args''' is not given, this function automatically extracts beacon keys-values from request parameters and {{code|lang=python|request.data}} (or {{code|lang=python|request.form['bdata']}}). | |||
|- style="vertical-align:top;" | |||
| '''Returns''' || | |||
* '''beacon_args''' ''(dict)'': Beacon keys-values repacked in the easily accessible dict form. | |||
|- style="vertical-align:top;" | |||
| '''Notes''' || | |||
The function throws a '''461 exception (Session-time Error)''' if the beacon value '''time_diff''' is invalid or does not agree with the session time. | |||
|} | |||
The process of locking/unlocking a document can also be automated via another function '''{{code|lang=python|process_doclock}}'''. | |||
'''<syntaxhighlight lang="python" id="process_doclock"> | |||
process_doclock(beacon_args, module_name, perm_name, headmodel=None, paired_gl=None) | |||
</syntaxhighlight>''' | |||
{| style="margin-left:20px;" | |||
|- style="vertical-align:top;" | |||
| style="width:120px;" | '''Parameters''' || | |||
* '''beacon_args''' ''(dict)'': Beacon keys-values as a dict. Usually this dict is what returned from '''{{code|process_beacon_args}}''' | |||
* '''module_name''' ''(str)'': The code of the CuneiFox module that the current operation belongs to. This argument is used along side '''perm_name''' to navigate for the user's permission bit. | |||
* '''perm_name''' ''(str)'': The permission bit name related to the current operation. | |||
* '''headmodel''' ''(CuneiModel)'': The model of the document's master record (usually the document header). If this argument is not given, the document locking/unlocking is skipped. | |||
* '''paired_gl''' ''(list)'': The specification for the accounting journal database corresponding to the current operation. The member's of the list are: | |||
*# '''glname''': The account journal name in the form of '''{{code|lang=python|'GL_<code>'}}''' (for example, GL_PAY, GL_REC). | |||
*# '''gl_headmodel''': The CuneiModel for the journal header. | |||
*# '''paired_col_doc''' ''(optional, defaults to {{code|lang=python|'docno'}})'': The reference column in '''headmodel'''. | |||
*# '''paired_col_gl''' ''(optional, defaults to {{code|lang=python|'docno'}})'': The reference column in '''gl_headmodel'''. | |||
*# '''lock_fld''' ''(optional, defaults to {{code|lang=python|'lock'}})'': The journal lock column in '''gl_headmodel'''. | |||
|- style="vertical-align:top;" | |||
| '''Returns''' || | |||
* '''{{code|lang=python|None}}''' if the locking/unlocking is skipped. | |||
* '''{{code|lang=python|403}}''' if the user does not have the valid permission to initiate the document lock. | |||
* '''beacon_args''': If the locking/unlocking is successful, the function spits back the '''beacon_args''' as given. | |||
|} | |||
==== Page Permission → Document Lock → Allowed Actions ==== | |||
In a sense, the actions that a user can perform on a page are dictated by the user's permission. However, in the messy real world, permission bits are no the only factor at play here. In CuneiFox, allowed actions are determined as follows: | |||
# '''When a user initiates a page-level action,''' CuneiFox first determines whether the user is authorized to do so ''(via a permission bit check)''. Then, | |||
#* For an '''edit''' operation, the program checks whether the document is locked by another user. If not, the document lock is applied under the current user's name. | |||
#* For a '''delete''' operation, the user is only allowed to proceed if the document is not locked by another user. | |||
#* For an '''add''' operation, the program automatically proceeds. The document lock is to be applied automatically once the document header is committed to database. | |||
# '''When a user submits a CuneiForm''' ''(a stand-alone form, an in-line form, a CuneiTable's full-form)'', the recommended template has CuneiFox re-interpret the permission bit and the document lock state into allowed action dictionary via Python functions '''{{code|lang=python|check_doclock}}''' and '''{{code|lang=python|process_allowed_action}}'''. The form submission is only processed if the operation is allowed. | |||
'''<syntaxhighlight lang="python"> | |||
check_doclock(datatype, bookcode, headmodel, form, field, | |||
doc_perm_bit, head_id_col="id", paired_gl=None) | |||
</syntaxhighlight>''' | |||
{| style="margin-left:20px;" | |||
|- style="vertical-align:top;" | |||
| style="width:120px;" | '''Description''' || Based on the current lock-state of a particular document, returns the dict of actions the user is allowed to perform at a header level. | |||
|- style="vertical-align:top;" | |||
| '''Parameters''' || | |||
* '''datatype''' ''(str: {{code|lang=python|'table'}}, {{code|lang=python|'data'}}, or {{code|lang=python|'report'}})'': The data type (location) of the database file. | |||
* '''bookcode''' ''(str)'': The name of the database file. In CuneiFox convention, this is usually a 3-lettered string: ''{{code|lang=python|'XXX'}} for general data, {{code|lang=python|'GL_XXX'}} for accounting journal data.'' | |||
* '''headmodel''' ''(CuneiModel)'': The model of the document's master record (usually the document header). | |||
* '''form''' ''(CuneiForm)'': The form currently being submitted. | |||
* '''field''' ''(str)'': The name of the form field where the document ID can be retrieved. (Usually this corresponds to the IntegerField '''id''' for the header form, or the ForeignKeyField '''doc''' for detail/supplementary forms.) | |||
* '''doc_perm_bit''' ''(int)'': The user's page-level permission bit for this operation. | |||
* '''head_id_col''' ''(str)'': The column name for the document ID in '''headmodel'''. | |||
* '''paired_gl''' ''(list)'': (See details under [[#process_doclock]].)'' | |||
|- style="vertical-align:top;" | |||
| '''Returns''' || | |||
* '''{{code|lang=python|403}}''' if the user's permission is lower than {{code|lang=python|2}}. | |||
* '''head_action_dic''': A dict in the format {{code|lang=python|{'add':True, 'edit':False, 'del':False} }}. Each of the 3 Boolean values in the dict indicates whether its corresponding action is allowed at the header level. | |||
|- style="vertical-align:top;" | |||
| '''Notes''' || The function throws a '''491 exception (Frozen-data Error)''' or '''492 exception (Posted-data Error)''' if the month's data is frozen or locked respectively. | |||
|} | |||
'''<syntaxhighlight lang="python"> | |||
process_allowed_action(head_action_dic, is_head) | |||
</syntaxhighlight>''' | |||
{| style="margin-left:20px;" | |||
|- style="vertical-align:top;" | |||
| style="width:120px;" | '''Description''' || Interprets the '''head_action_dic''' (returned from '''{{code|check_doclock}}''') and whether the current form is for the document header, returns a new '''allowed_action''' dict that is really applicable to the current form. | |||
By CuneiFox standard, all actions is allowed for the sub-entries if the user holds the ''edit'' permission at the header level (including the temporary ''edit'' permission granted upon document addition.) | |||
|- style="vertical-align:top;" | |||
| '''Parameters''' || | |||
* '''head_action_dic''' ''(dict)'': Allowed actions at the header level. | |||
* '''is_head''' ''(bool)'': Whether the currently submitted form corresponds with the document's master entry. | |||
|- style="vertical-align:top;" | |||
| '''Returns''' || | |||
* '''{{code|lang=python|409}}''' if the no actions are allowed. At this stage, this is mostly caused by another user's lock on the document. | |||
* '''allowed_action''': A dict in the format {{code|lang=python|{'add':True, 'edit':True, 'del':True} }}. Each of the 3 Boolean values in the dict indicates whether its corresponding action is allowed. | |||
|} | |||
== Define a Document Context == | == Define a Document Context == | ||
บรรทัดที่ 13: | บรรทัดที่ 159: | ||
=== Main Sequence Keys === | === Main Sequence Keys === | ||
==== Page Permission & Sequence Definition ==== | |||
* '''mainseq''' ''([[CuneiForm/CuneiTable,],]: defaults to {{code|lang=python|[]}})'': The list of page sequence specifications. Each member is a list of CuneiForms and/or CuneiTables belonging to that specific sequence. | |||
* '''mainseq_name''' ''([[str,],]: defaults to {{code|lang=python|[]}})'': Basically '''mainseq''', but repacked with '''_id''' attributes ''(str)'' of each CuneiForm/CuneiTable in place of the object itself. ''(This value is automatically created can only used for ease of internal references.)'' | |||
* '''page_perm''' ''(int: defaults to {{code|lang=python|1}})'': Page-level permission value. | * '''page_perm''' ''(int: defaults to {{code|lang=python|1}})'': Page-level permission value. | ||
** '''0''': Not readable. (Usually users with this permission level should be prevented from reaching the page at all. '''DO NOT''' rely on this permission bit as a security measure,) | ** '''0''': Not readable. (Usually users with this permission level should be prevented from reaching the page at all. '''DO NOT''' rely on this permission bit as a security measure,) | ||
บรรทัดที่ 20: | บรรทัดที่ 169: | ||
** '''4''': Add + Edit + Delete. | ** '''4''': Add + Edit + Delete. | ||
** '''≥5''': Add + Edit + Delete + Import. | ** '''≥5''': Add + Edit + Delete + Import. | ||
* ''' | * '''proceed_lock_seq''' ''([[str,],]: defaults to {{code|lang=python|[]}})'': List of document verification items that should prevent proceeding/canceling away from each sequence. | ||
* ''' | * '''searchers''' ''[CuneiTable,]: defaults to {{code|lang=python|[]}}'': List of search tables. | ||
==== Basic Actions ==== | |||
* '''mainseq_add''' ''(str: defaults to {{code|lang=python|'plain'}})'': Action type when the page-level Add button is clicked. | * '''mainseq_add''' ''(str: defaults to {{code|lang=python|'plain'}})'': Action type when the page-level Add button is clicked. | ||
** '''{{code|lang=python|'newmaster'}}''': The button triggers the addition of a new master entry. ''(This setting assumes the existence of a [[#Page Sequence|Master Form]] and certain [[# | ** '''{{code|lang=python|'newmaster'}}''': The button triggers the addition of a new master entry. ''(This setting assumes the existence of a [[#Page Sequence|Master Form]] and certain [[#Mass Populate Route|Mass Populate]] setting.)'' | ||
** '''Any other string''': The button triggers the function {{code|lang=javascript|this_page_add(event)}}. This function must be custom written for each page that needs it. | ** '''Any other string''': The button triggers the function {{code|lang=javascript|this_page_add(event)}}. This function must be custom written for each page that needs it. | ||
* '''mainseq_del''' ''(str: defaults to {{code|lang=python|'plain'}})'': Action type when the page-level Delete button is clicked. | * '''mainseq_del''' ''(str: defaults to {{code|lang=python|'plain'}})'': Action type when the page-level Delete button is clicked. | ||
** '''{{code|lang=python|'delmaster'}}''': The button triggers the prompt for the deletion of the current master entry. ''(This setting assumes the existence of a [[#Page Sequence and Master Object|Master Form]] and certain [[# | ** '''{{code|lang=python|'delmaster'}}''': The button triggers the prompt for the deletion of the current master entry. ''(This setting assumes the existence of a [[#Page Sequence and Master Object|Master Form]] and certain [[#Mass Populate Route|Mass Populate]] setting.)'' | ||
** '''Any other string''': The button triggers the function {{code|lang=javascript|this_page_delete(event)}}. This function must be custom written for each page that needs it. | ** '''Any other string''': The button triggers the function {{code|lang=javascript|this_page_delete(event)}}. This function must be custom written for each page that needs it. | ||
* '''mainseq_proceed''' ''([str,]: defaults to {{code|lang=python|[]}})'': Each sequence's action type when the page-level Proceed button is clicked. | * '''mainseq_proceed''' ''([str,]: defaults to {{code|lang=python|[]}})'': Each sequence's action type when the page-level Proceed button is clicked. | ||
** '''{{code|lang=python|'submit'}}''': Submit the first CuneiForm in the current page sequence. | ** '''{{code|lang=python|'submit'}}''': Submit the first CuneiForm in the current page sequence. '''Note that''' this proceeding mode '''DOES NOT''' transmit a 'cancel' beacon. So, if the final sequence of the page has this proceed mode, the duty of unlocking the document falls on the submission route of the final form. | ||
** '''{{code|lang=python|'beacon'}}''': Beam a 'proceed' beacon to the server to signal a page sequence change. (For the final page sequence, a 'cancel' beacon is sent instead to signal checking out of the page's edit mode.) | ** '''{{code|lang=python|'beacon'}}''': Beam a 'proceed' beacon to the server to signal a page sequence change. (For the final page sequence, a 'cancel' beacon is sent instead to signal checking out of the page's edit mode.) | ||
* '''mainseq_search''' ''(str: defaults to {{code|lang=python|False}})'': Field name of the [[#Page Sequence|Master Form]] used for page-level search. | * '''mainseq_search''' ''(str: defaults to {{code|lang=python|False}})'': Field name of the [[#Page Sequence|Master Form]] used for page-level search. | ||
* '''mainseq_schprmpt''' ''(str: defaults to {{code|lang=python|'Document Number'}})'': Prompt text for page-level search. | * '''mainseq_schprmpt''' ''(str: defaults to {{code|lang=python|'Document Number'}})'': Prompt text for page-level search. | ||
==== Request-based Actions ==== | |||
* '''mainseq_beacon''' ''(url_for_string: defaults to {{code|lang=python|False}})'': Route for beacon transmission. | |||
* '''mainseq_print''' ''(url_for_string: defaults to {{code|lang=python|False}})'': Route for print request. | |||
* '''mainseq_qr''' ''(url_for_string: defaults to {{code|lang=python|False}})'': Route for Payment QR-code request. | |||
* '''mainseq_vouch''' ''(url_for_string: defaults to {{code|lang=python|False}})'': Route to corresponding Accounting Journal record. ''(Usually directed through {{code|cunei_gl.direct_to_vouch}}.)'' | |||
* '''mainseq_export''' ''(url_for_string: defaults to {{code|lang=python|False}})'': Route to export request. | |||
==== Form-based Actions ==== | |||
* '''mainseq_modaldel''' ''(CuneiForm: defaults to {{code|lang=python|False}})'': If defined, right-clicking on the page-level Delete button triggers a CuneiModal containing this form. The standard '''Mass Deletion Form''' is pre-defined at '''cuneifox.base.main.forms.StdModalDelForm'''. | |||
* '''mainseq_copy''' ''(CuneiForm: defaults to {{code|lang=python|False}})'': If defined, clicking on the page-level Copy button triggers a CuneiModal containing this form. The standard '''Document Copy Form''' is pre-defined at '''cuneifox.base.main.forms.StdCopyForm'''. | |||
* '''mainseq_attach''' ''(CuneiForm: defaults to {{code|lang=python|False}})'': If defined, clicking on the page-level Attach button triggers a CuneiModal containing this form. The standard '''Document Attach Form''' is pre-defined at '''cuneifox.base.main.forms.StdAttachForm'''. | |||
* '''mainseq_modalprint''' ''(CuneiForm: defaults to {{code|lang=python|False}})'': If defined, right-clicking on the page-level Print button triggers a CuneiModal containing this form. The standard '''Custom Printing Form''' is pre-defined at '''cuneifox.base.main.forms.StdModalPrintForm'''. | |||
* '''mainseq_upload''' ''(CuneiForm: defaults to {{code|lang=python|False}})'': If defined, clicking on the page-level Import button triggers a CuneiModal containing this form. The standard '''Document Import Form''' is pre-defined at '''cuneifox.base.main.forms.StdImportForm'''. | |||
<syntaxhighlight lang="python"> | |||
# Example from Accounting Journal (w/ Input VAT) page | |||
# MAIN SEQUENCE FORMS/TABLES | |||
head_form = VoucherHeadForm.init_form(prefix="VoucherHeadForm", | |||
gen_del="cunei_gl", sequence_bound=True, | |||
populate_id=["id"], populate_suppress=["grab"], | |||
post_route=url_for("<head_submit_route>"), ...) | |||
vouch_tb = VouchBodyTable(prefix="VouchBodyTable", editable=True, perm_bit=4, | |||
populate_route=url_for("<vouch_default_val_request_route>"), | |||
populate_id=["master.id"], populate_suppress=["qsch", "free_grab"], | |||
post_route=url_for("<vouch_submit_route>"), ...) | |||
vouchsum_form = VoucherDetailSum.init_form(prefix="VoucherDetailSum", ...) | |||
vat_tb = VouchVatTable(prefix="VouchVatTable", editable=True, perm_bit=4, | |||
populate_route=url_for("<vat_default_val_request_route>"), | |||
populate_id=["master.id", "#master.section_stid", | |||
"#master.docdate", "#master.desc"], | |||
populate_suppress=["qsch", "free_grab"], | |||
post_route=url_for("<vat_submit_route>"), ...) | |||
vatsum_form = VatDetailSum.init_form(prefix="VatDetailSum", ...) | |||
wht_tb = VouchWhtTable(prefix="VouchWhtTable", editable=True, perm_bit=4, | |||
populate_route=url_for("<wht_default_val_request_route>"), | |||
populate_id=["master.id", "#master.section_stid", | |||
"#master.docdate", "#master.desc"], | |||
populate_suppress=["qsch", "free_grab"], | |||
post_route=url_for("<wht_submit_route>"), ...) | |||
whtsum_form = WhtDetailSum.init_form(prefix="WhtDetailSum", ...) | |||
==== | # SEARCH TABLES | ||
# (Note that most search tables use unified 'Search Table with a Stand-alone Page'. | |||
# See corresponding section under CuneiTable page for the template.) | |||
section_tb = SectionTable(prefix="SectionTable", perm_bit=<user_perm_for_section>, | |||
populate_route=url_for("cunei_gl.section", fetch="yes"), | |||
post_route=url_for("cunei_gl.section"), | |||
in_modal="Modal0", modal_head=lazy_gettext("Choose Section"), ...) | |||
acccode_tb = AccCodeTable(prefix="AccCodeTable", perm_bit=<user_perm_for_acccode>, | |||
populate_route=url_for("cunei_gl.acccode", fetch="yes"), | |||
post_route=url_for("cunei_gl.acccode"), | |||
in_modal="Modal1", modal_head=lazy_gettext("Choose Account Code"), ...) | |||
... | |||
# START CREATING DOCUMENT CONTEXT | |||
mainseq = [[head_form], | |||
[vouch_tb, vat_tb, wht_tb, vouchsum_form, vatsum_form, whtsum_form]] | |||
attach_form = StdAttachForm.gen_attach_form(post_route=url_for("<attach_submit_route>"), | |||
populate_route=url_for("<attach_fetch_route>")) | |||
modalprint_form = StdModalPrintForm.gen_modalprint_form(post_route=url_for("<custom_prn_submit_route>")) | |||
modaldel_form = StdModalDelForm.gen_modaldel_form(post_route=url_for("<mass_del_submit_route>")) | |||
modalcopy_form = StdCopyForm.gen_copy_form(url_for("<copy_submit_route>"), ...), | |||
modalimport_form = StdImportForm.gen_import_form(url_for("<import_route>")) | |||
# USE 'pack_mainseq' TO FILL UNSPECIFIED KEYS WITH DEFAULT VALUES. | |||
mainseq_args = pack_mainseq(mainseq=mainseq, | |||
mainseq_add="newmaster", | |||
mainseq_del="delmaster", | |||
page_perm=<user_perm_for_the_page>, | |||
mainseq_proceed=["submit", "beacon"], | |||
proceed_lock_seq=[[], ["incomp"]], | |||
mainseq_search="docno", | |||
mainseq_print=url_for("<print_route>"), | |||
mainseq_export=url_for("<export_route>"), | |||
mainseq_beacon=url_for("<beacon_route>"), | |||
mainseq_copy=modalcopy_form, | |||
mainseq_attach=attach_form, | |||
mainseq_modalprint=modalprint_form, | |||
mainseq_modaldel=modaldel_form, | |||
mainseq_upload=modalimport_form, | |||
searchers=[section_tb, acccode_tb, ...]) | |||
</syntaxhighlight> | |||
=== Additional Document Context Keys === | === Additional Document Context Keys === | ||
บรรทัดที่ 75: | บรรทัดที่ 294: | ||
<div id="img001">[[File:Document page sample.png|720px|thumb|center|alt=Example of a Document Page|'''Example of a Document Page:''' (1) Document Counter, (2) Document Verification, (3)~(4) Page Button Sets]]</div> | <div id="img001">[[File:Document page sample.png|720px|thumb|center|alt=Example of a Document Page|'''Example of a Document Page:''' (1) Document Counter, (2) Document Verification, (3)~(4) Page Button Sets]]</div> | ||
=== | === Mass Populate Route === | ||
''See more detailed discussion on [[Page#Mass Populate|Mass Populate]] under Page.'' | |||
The mass populate route is not defined as a part of the Document Context and is fed directly to the {{code|render_template}} function. Although its use is not limited to multi-component pages, it has enough exclusivity to warrant some discussion here. | |||
* ''' | Most document pages within CuneiFox system have their '''mass_populate''' set as: | ||
* ''' | <syntaxhighlight lang="python"> | ||
* ''' | mass_populate = ["<mass_populate_route>", | ||
* ''' | ["master.id", "window.pagenav"]] | ||
</syntaxhighlight> | |||
* The trigger '''{{code|lang=python|'master.id'}}''' refers to the (usually hidden) '''id''' field of the master (document header) form. | |||
* The trigger '''{{code|lang=python|'window.pagenav'}}''' refers to the hidden {{code|lang=html|<div>}} named '''{{code|lang=javascript|'pagenav'}}'''. | |||
==== How {{code|pagenav}} Works ==== | |||
The hidden element {{code|lang=javascript|'pagenav'}} is a very common trigger for mass populating routine. When set, the pagenav trigger works as follows: | |||
# The {{code|lang=javascript|documentElement}} and Arrow buttons in the navigation button set listen to certain {{code|keydown}} or {{code|click}} events, then change the {{code|innerHTML}} of {{code|pagenav}} accordingly: | |||
#* [[ไฟล์:Cuneifox first btn.png|25px|frameless|alt=First button|First button]] ( {{key press|Home}} ): Changes the value to '''{{code|lang=javascript|-2}}''' | |||
#* [[ไฟล์:Cunefox prev btn.png|25px|frameless|alt=Previous button|Previous button]] ( {{key press|PageUp}} ): Changes the value to '''{{code|lang=javascript|-1}}''' | |||
#* [[ไฟล์:Cuneifox next btn.png|25px|frameless|alt=Next button|Next button]] ( {{key press|PageDown}} ): Changes the value to '''{{code|lang=javascript|1}}''' | |||
#* [[ไฟล์:Cuneifox last btn.png|25px|frameless|alt=Last button|Last button]] ( {{key press|End}} ): Changes the value to '''{{code|lang=javascript|2}}''' | |||
# A {{code|change}} event is then triggered for the pagenav element. This, in turn, triggers the page's mass population. | |||
# The mass population routine involves collecting values from all the triggers to send along side the request. These values, including the {{code|innerHTML}} of pagenav, is interpreted on the server-side. | |||
== Fit the Context on an HTML Page == | == Fit the Context on an HTML Page == | ||
== Page-level | === Sequence Indicator === | ||
=== Page-level Post Populate Functions === | |||
=== Commonly-used HTML Blocks === | |||
It cannot be denied that CuneiFox templates form a rather comlex system; setting up a new HTML page, especially one with multiple inter-dependent pieces, from scratch can be tedious. In this section, we assume the use of CuneiFox's own '''{{code|layout.html}}''' as the starting point. | |||
<syntaxhighlight lang="python" id="process_beacon_args"> | |||
WAIT | |||
</syntaxhighlight> | |||
In practice, however, one might find it a little easier to start with '''{{code|greeter_slim.html}}''' instead. ''(See [[#HTML Page Pattern]].)'' | |||
== Useful Patterns == | == Useful Patterns == | ||
=== Standard Forms === | |||
=== Unified Render-Fetch-Submit Route === | |||
<syntaxhighlight lang='python' line=1> | |||
@<blueprint>.route("<unified_route>", methods=["GET", "POST"]) | |||
@login_required | |||
def <function_name>(*args): | |||
# CHECK FOR MODULE ACTIVATION | |||
check_modact_stat(modcode=pr_moddata["code"], mode="menu", response="abort") | |||
# PERFORM PERMISSION CHECK | |||
# ... | |||
# SCREEN OUT THE PAGE-LEVEL SEARCH REQUEST FIRST | |||
page_search_kw = request.args.get("page_search", None) | |||
if not(page_search_kw is None): | |||
goal_id = grab_id(...) | |||
if goal_id is None: | |||
return redirect(url_for("<unified_route>")) | |||
else: | |||
return redirect(url_for("<unified_route>", id=goal_id)) | |||
# PREPARE THE PAGE'S FORMS AND TABLES. | |||
# (Depending on the type of the request, seach tables may or may not be created here.) | |||
# Pack those along with other 'doc_context'. | |||
# ... | |||
# ... | |||
# ... | |||
# DISTINGUISH THE REQUEST INTO ONE OF FETCH, SUBMIT, OR PAGE-GET | |||
# ... | |||
### Cases of fetch and submit usually comes with a form/table pointer. | |||
### This pointer should be encoded in the populate routes of the object/page. | |||
fetch_tbfm = request.args.get("tbfm") if request.args.get("tbfm") else None | |||
if <is_fetch>: | |||
identifiers = json.loads(request.form.get("identifier")) | |||
if fetch_tbfm == "_all": # This usually marks a MASS_POPULATE request. | |||
thisdoc, doccount, flash = grab_whole_doc(...) | |||
if thisdoc[head_form._id] is None: # No document found | |||
# Usually just fill all fields of the header with None | |||
thisdoc[head_form._id] = {col:None for col in head_form.seq} | |||
elif thisdoc[head_form._id] == "ncol": # Default for new document requested | |||
thisdoc[head_form._id] = dict(...) | |||
else: # Found a document | |||
thisdoc[head_form._id] = {col:getattr(thisdoc[head_form._id], col, None) \ | |||
for col in head_form.seq} | |||
# Modify as needed. | |||
# ... | |||
# It should be noted here that a database fetch returns CuneiModel instances. | |||
# These instances need to be repackaged into DICT for forms and LIST-OF-LIST for tables. | |||
# ... | |||
# temp_dicts = [a.m2d(int_key=["doc"]) for a in thisdoc[<tb>._id]] | |||
# thisdoc[<tb>._id] = [[a.get(col, None) for col in <tb>.cnames] for a in temp_dicts] | |||
# ... | |||
# Some forms/tables are filled with freshly calculated values. Pack those here. | |||
# ... | |||
# DETERMINE DOCUMENT VERIFICATION STATE and its implication the page-level actions. | |||
docverify = dict(...) | |||
if doccount[1] <= 0 or <some_other_conditions>: | |||
editdisable = ["edit", "del"] | |||
else: | |||
editdisable = [] | |||
return jsonify({"data":thisdoc, "doccount":doccount, "docverify":docverify, | |||
"flash":flash, "editdisable":editdisable}) | |||
# Besides MASS_POPULATE (and properly suppresses), multi-component pages | |||
# almost exclusively request default data set for each table. | |||
# ** A table's default is returned in a LIST form. | |||
elif fetch_tbfm == "<table_marker>": | |||
if request.form.get("mode") == "ncol": | |||
return jsonify({"data":[...]}) | |||
elif fetch_tbfm == "<another_table_marker>": | |||
if request.form.get("mode") == "ncol": | |||
return jsonify({"data":[...]}) | |||
elif issubmit: | |||
# CHECK DOCUMENT'S LOCK STATE AND ALLOWED ACTIONS | |||
head_action_dic = check_doclock(...) | |||
if isinstance(head_action_dic, int): | |||
return jsonify(message='Input 403 message here'), head_action_dic | |||
allowed_action = process_allowed_action(head_action_dic, fetch_tbfm=="head") | |||
if allowed_action == 409: | |||
return jsonify(message='Input 409 message here'), 409 | |||
# PERFORM FORM VALIDATION AND DATABASE COMMIT VIA 'GEN_DBCOMMIT_RESP' | |||
db_mod_code, new_entry, resp_dict = gen_dbcommit_resp(...) | |||
if resp_dict == 403: | |||
abort(403) | |||
# IF 'NEW_ENTRY' IS PRESENT, THE CHANGE WAS COMMITTED TO DATABASE. | |||
# Modify 'resp_dict' as needed. Only necessary modifications are illustrated below. | |||
if not(new_entry is None): | |||
# THE WHOLE DOCUMENT MIGHT OR MIGHT NOT BE RE-FETCHED. | |||
# _, fetched_doc = prefetch_all_docs(...) | |||
if fetch_tbfm == "head": | |||
if db_mod_code == 4: # This is the case of HEADER DELETION. | |||
resp_dict["repop"] = gen_form_skipper(head_form, {"id":-2}) | |||
resp_dict["del_master"] = True | |||
else: | |||
if db_mod_code == 2: # This is the case of NEWLY ADDED HEADER. | |||
resp_dict["repop"] = gen_form_skipper(head_form, {"id":new_entry.get("id", None)}) | |||
elif fetch_tbfm == "<table_marker>": | |||
# Data committed to sub-tables is repackaged for the client. | |||
if isinstance(db_mod_code, int) and db_mod_code < 4: | |||
resp_dict["entry"] = [new_entry.get(col, None) for col in bill_tb.cnames] | |||
# Most sub-table commits require re-calculations of sum values shown | |||
# on the client side as well. | |||
resp_dict["crossfill"] = {<table_sum_form>._id:dict(...)} | |||
# POST-COMMIT ROUTINE GOES HERE. | |||
# ... | |||
# ... | |||
return jsonify(resp_dict) | |||
# NOW ONLY PAGE-GET REQUEST REMAINS! | |||
# ... | |||
# This line works solo and along-side the page-level search from above. | |||
head_form.id.data = request.args.get("id", "") | |||
# ... | |||
title = lazy_gettext("...") | |||
return render_template("<template_file_name>", title=title, greeter=title, | |||
mass_populate=..., pin_args=[...], perm_to_show=cur_perm, | |||
doc_context=dict(btn_set=True, doccount=True, docverify=dict(...), | |||
**mainseq_args)) | |||
</syntaxhighlight> | |||
=== Beacon Route === | |||
<syntaxhighlight lang='python' line=1> | |||
@<blueprint>.route("<beacon_route>", methods=["POST"]) | |||
def <function_name>(*args): | |||
# UNPACK BEACON KEYS/VALUES | |||
beacon_args = process_beacon_args(...) | |||
# ... | |||
# PROCESS DOCLOCK. Aborting is acceptable. | |||
# ... | |||
lock_stat = process_doclock(beacon_args, ...) | |||
if lock_stat == 403: | |||
abort(403) | |||
# (OPTIONAL) ADDITIONAL ROUTINES FOR THIS DOCUMENT TYPE | |||
# ... | |||
# ... | |||
# FINALLY, when all is done successfully, | |||
# return something (a string is acceptable). | |||
return "YES" | |||
</syntaxhighlight> | |||
=== Copy Route === | |||
<syntaxhighlight lang='python' line=1> | |||
@<blueprint>.route("<copy_route>", methods=["GET", "POST"]) | |||
def <function_name>(*args): | |||
# RE-CREATE THE COPY FORM AND UNPACK COPY PARAMETERS | |||
# ... | |||
cp_form = StdCopyForm.gen_copy_form(...) | |||
# VIA PREP_COPY, GET THE FULL ENTRY (WITH SUB-ENTRIES) OF THE ORIGINAL DOC. | |||
# THE FUNCTION ALSO INTERPRETS COPY PARAMETERS FROM THE FORM AS WELL. | |||
return_dict, orig_doc, copy_params = prep_copy(cp_form, ...) | |||
if orig_doc is None: | |||
return jsonify(return_dict) | |||
# MANIPULATE THE DOCUMENT INTO THE NEWLY COPIED FORM. | |||
# (Note that not all values are copied verbatim.) | |||
new_docdate = copy_params["docdate"] | |||
# ... | |||
# CREATE A COPY OF THE DOCUMENT VIA CREATE_DOC_COPY | |||
new_head = create_doc_copy(cp_form, copy_params, ...) | |||
if new_head is None: | |||
return_dict["err"] = {"docno":[lazy_gettext("Copy error!")]} | |||
return jsonify(return_dict) | |||
# CHANGE THE SESSION DATE-TIME, THEN PERFORM ANY NEEDED POST-COPY ROUTINE | |||
# (e.g. debt history, inventory management, etc.) | |||
is_success, _, timediff = change_datetime(form=None, date_obj=new_head.docdate) | |||
if is_success: | |||
session["time_diff"] = timediff | |||
# ... | |||
# FINALLY, PREPARE THE REDIRECT ROUTE (usually the same route with the new | |||
# docid specified) AND RETURN | |||
return_dict["redirect"] = url_for("<redirect_route>", id=new_head.id, ...) | |||
return jsonify(return_dict) | |||
</syntaxhighlight> | |||
=== Unified Attach Route === | |||
<syntaxhighlight lang='python' line=1> | |||
@<blueprint>.route("<attach_route>", methods=["GET", "POST"]) | |||
def <function_name>(*args): | |||
# CHECK FOR MODULE ACTIVATION | |||
check_modact_stat(...) | |||
# CONDUCT PERMISSION CHECK | |||
# ... | |||
# CREATE AN ATTACH FORM AND PROCESS THE SUBMITTED RESULT | |||
at_form = StdAttachForm.gen_attach_form() | |||
return process_doc_file(at_form, ...) | |||
</syntaxhighlight> | |||
=== Document Mass-delete Route === | |||
<syntaxhighlight lang='python' line=1> | |||
@<blueprint>.route("<massdel_route>", methods=["GET", "POST"]) | |||
def <function_name>(*args): | |||
@copy_current_request_context | |||
def <proxy_function>(*proxy_args): | |||
try: | |||
has_err = doc_mass_delete(job_token=job_token, ...) | |||
except Exception: | |||
update_task_db(job_token, new_stat="killed", ...) | |||
# CHECK FOR MODULE ACTIVATION | |||
check_modact_stat(...) | |||
# PERFORM PERMISSION CHECK | |||
# ... | |||
# DETERMINE THE LIST OF DOCUMENTS TO BE DELETED | |||
# (This example illustrates this step via 'std_modaldel_to_docno_list'). | |||
resus_token = request.args.get("resus_token", None) | |||
immediate_return, return_dict, doccount, thesedocs = \ | |||
StdModalDelForm.std_modaldel_to_docno_list(..., resus_token=resus_token) | |||
if not(immediate_return): # Form validation succeeds | |||
job_token = gen_random_token(10) if resus_token is None else resus_token | |||
new_task = init_task_db(job_token, 1, "request", suppress_request=False) | |||
if new_task is None: | |||
raise current_app.custom_exceptions.default_exceptions[410]() | |||
if new_task.status_str != "queue": | |||
job_resuscitation = False | |||
threading.Thread(target=<proxy_function>, name=job_token, args=[...]).start() | |||
if not(resus_token is None): | |||
return "YES" | |||
else: | |||
job_resuscitation = url_for("<massdel_route>", resus_token=job_token, ...) | |||
update_task_db(job_token, | |||
further_action=pjson.dumps({"docids":[mem.id for mem in thesedocs]})) | |||
return jsonify({"redirect": url_for("main.delete_progress", job_token=job_token, | |||
job_resuscitation=job_resuscitation, ...)}) | |||
return jsonify(return_dict) | |||
</syntaxhighlight> | |||
=== Unified Print & QR Code Route === | |||
<syntaxhighlight lang='python' line=1> | |||
@<blueprint>.route("<print_route>", methods=["GET", "POST"]) | |||
def <function_name>(*args): | |||
@copy_current_request_context | |||
def <proxy_function>(job_token, doc_objs, ...): | |||
# ... | |||
print_in_thread(job_token, doc_objs, pseudo_mapper={...}, ...) | |||
def <doc_modify_function>(doc_obj): | |||
method_dict = dict(paymethods) | |||
for r in doc_obj.route: | |||
r.paymethod = method_dict.get(r.paymethod, r.paymethod) | |||
# CHECK FOR MODULE ACTIVATION | |||
check_modact_stat(...) | |||
# THIS FIRST BLOCK IS FOR PRINTING VIA THE PRINT-MODAL | |||
if request.args.get("modalprint", "no") == "yes": | |||
resus_token = request.args.get("resus_token", None) | |||
immediate_return, return_dict, chosen_template, make_pdfa3, doccount, thesedocs = \ | |||
StdModalPrintForm.std_modalprint_to_docno_list(..., resus_token=resus_token) | |||
if not(immediate_return): # Form validation succeeds | |||
job_token = gen_random_token(10) if resus_token is None else resus_token | |||
new_task = init_task_db(job_token, 1, "request", suppress_request=False) | |||
if new_task is None: | |||
raise current_app.custom_exceptions.default_exceptions[410]() | |||
if new_task.status_str != "queue": | |||
for d in thesedocs: | |||
<doc_modify_function>(d) | |||
job_resuscitation = False | |||
threading.Thread(target=<proxy_function>, name=job_token, args=[...]).start() | |||
if not(resus_token is None): | |||
return "YES" | |||
else: | |||
job_resuscitation = url_for("<print_route>", resus_token=job_token, ...) | |||
update_task_db(job_token, | |||
further_action=pjson.dumps({"chosen_template":chosen_template, | |||
"make_pdfa3":make_pdfa3, | |||
"docids":[mem.id for mem in thesedocs]})) | |||
return_dict["new_tab"] = url_for("main.print_pdf", job_token=job_token, | |||
final_name="{}_out.pdf".format(current_user.code), | |||
job_resuscitation=job_resuscitation) | |||
return jsonify(return_dict) | |||
# BELOW THIS POINT IS THE NORMAL PRINT/QR ROUTINE | |||
try: | |||
docid = int(request.args.get("master.id")) | |||
except (ValueError, TypeError, KeyError): | |||
abort(404) | |||
_, thisdoc = prefetch_all_docs(...) | |||
if thisdoc is None: | |||
abort(404) | |||
if request.args.get("qr") == "yes": | |||
# ... | |||
qr_file = gen_qr(...) | |||
if qr_file == 404: | |||
abort(404) | |||
return send_file(qr_file) | |||
<doc_modify_function>(thisdoc) | |||
pdf_path = replace_printsvg(...) | |||
if pdf_path is None: | |||
abort(404) | |||
return render_template("print_pdf.html", pdf_path=pdf_path) | |||
</syntaxhighlight> | |||
=== Document Export Route === | |||
<syntaxhighlight lang='python' line=1> | |||
@<blueprint>.route("<export_route>", methods=["GET", "POST"]) | |||
def <function_name>(*args): | |||
# CHECK FOR MODULE ACTIVATION | |||
check_modact_stat(modcode=pr_moddata["code"], mode="menu", response="abort") | |||
# FETCH THE DOCUMENTS & MODIFY THEM IF NEEDED | |||
_, thesedocs = prefetch_all_docs(...) | |||
thesedocs = [] if thesedocs is None else thesedocs | |||
# ... | |||
# EXPORT VIA 'GEN_SPREADSHEET' | |||
ss_path = gen_spreadsheet(...) | |||
if ss_path is None: | |||
abort(404) | |||
return render_template("serve_file.html", file_path=ss_path, dl_name="cunei_export.xlsx") | |||
</syntaxhighlight> | |||
=== Document Import Route === | |||
<syntaxhighlight lang='python' line=1> | |||
@<blueprint>.route("<import_route>", methods=["GET", "POST"]) | |||
def <function_name>(*args): | |||
@copy_current_request_context | |||
def <proxy_function>(job_token, imported_file, ...): | |||
try: | |||
has_err = docimport_to_db(imported_file, ..., job_token=job_token) | |||
except Exception as e: | |||
update_task_db(job_token, new_stat="killed", ...) | |||
remove(imported_file) | |||
# CHECK FOR MODULE ACTIVATION | |||
check_modact_stat(modcode=iv_moddata["code"], mode="menu", response="abort") | |||
# PERFORM PERMISSION CHECK | |||
# ... | |||
# RESUS_TOKEN IS PRESENT IN CASE OF A QUEUED OPERATION: | |||
# Either read the needed data from the form or read from stashed info. | |||
resus_token = request.args.get("resus_token", None) | |||
if resus_token is None: | |||
if not(import_form.validate_on_submit()): | |||
return jsonify({"err": import_form.errors}) | |||
file_path = prep_imported_file(import_form.filename) | |||
job_token = gen_random_token(10) | |||
else: | |||
file_path = read_resus_import_file(resus_token, will_abort=True) | |||
job_token = resus_token | |||
# ATTEMPT TO INITIATE THE OPERATION | |||
new_task = init_task_db(job_token, 1, "request", suppress_request=False) | |||
if new_task is None: | |||
raise current_app.custom_exceptions.default_exceptions[410]() | |||
# IF THE OPERATION CAN START IMMEDIATELY (NOT QUEUED): Run the routine. | |||
# IF NOT: Stash necessary info (esp. job_token) for future retrieval. | |||
if new_task.status_str != "queue": | |||
job_resuscitation = False | |||
threading.Thread(target=<proxy_function>, name=job_token, args=[...]).start() | |||
if not(resus_token is None): | |||
return "YES" | |||
else: | |||
job_resuscitation = url_for("<import_route>", resus_token=job_token, ...) | |||
update_task_db(job_token, further_action=pjson.dumps({"file_path":file_path})) | |||
return jsonify({"redirect": url_for("main.import_progress", job_token=job_token, | |||
job_resuscitation=job_resuscitation, ...)}) | |||
</syntaxhighlight> | |||
=== HTML Page Pattern === | |||
<syntaxhighlight lang="html" id="process_beacon_args" line="1"> | |||
<!-- EXAMPLE FROM ACCOUNTING JOURNAL PAGE --> | |||
{% import "cunei_forms.html" as CuneiForms %} | |||
{% import "cunei_tables.html" as CuneiTables with context %} | |||
{% import "cunei_modals.html" as CuneiModals with context %} | |||
{% import "cunei_scripts.html" as CuneiScripts %} | |||
{% import "cunei_icons.html" as CuneiIcons %} | |||
<!-- MANY DOCUMENT PAGES USE 'GREETER_SLIM' AS THE BASE. --> | |||
{% extends "greeter_slim.html" %} | |||
<!-- SET SOME NAMES FOR EASY REFERENCES. --> | |||
{% set head_form = doc_context["mainseq"][0][0] %} | |||
{% set vouch_tb = doc_context["mainseq"][1][0] %} | |||
{% set vat_tb = doc_context["mainseq"][1][1] %} | |||
{% set wht_tb = doc_context["mainseq"][1][2] %} | |||
{% set vouchsum_form = doc_context["mainseq"][1][3] %} | |||
{% set vatsum_form = doc_context["mainseq"][1][4] %} | |||
{% set whtsum_form = doc_context["mainseq"][1][5] %} | |||
{% block header %} | |||
<h3>{{ greeter }}</h3> | |||
{% endblock header %} | |||
{% block content_wgreeter %} | |||
{{ CuneiForms.general_form_head(head_form, is_master=true) }} | |||
<div class="col-sm-11"> | |||
<div class="card border-dark mb-3" style="max-width:100%;"> | |||
<div class="card-body py-0"> | |||
<div id="seq_indi_0" class="row align-items-start pt-3"> | |||
<!-- DRAW HEADER FORM HERE --> | |||
{{ CuneiForms.render_blank(head_form, head_form.id, hidden=true) }} | |||
<!-- ... --> | |||
<!-- ... --> | |||
{{ CuneiForms.render_submit(head_form, head_form.submit, hidden=true) }} | |||
{{ CuneiForms.render_check(head_form, head_form.is_del, hidden=true) }} | |||
{{ CuneiForms.render_submit(head_form, head_form.del_submit, hidden=true, | |||
class="danger", confirm_first=head_form.confirm_first) }} | |||
</div> | |||
<div id="seq_indi_1" class="row align-items-center mx-0 pt-1 pb-3"> | |||
<!-- DRAW BODY TABLES AND FORMS HERE --> | |||
<!-- ... --> | |||
{{ CuneiTables.general_table_block(vouch_tb, "max(100vh - 431px, 150px)", | |||
block_width="84vw", ...) }} | |||
{{ CuneiTables.general_table_tail(vouch_tb, false) }} | |||
<!-- ... --> | |||
{{ CuneiForms.general_form_head(vouchsum_form) }} | |||
{{ CuneiForms.render_blank(vouchsum_form, vouchsum_form.drsum, headless=true) }} | |||
{{ CuneiForms.render_blank(vouchsum_form, vouchsum_form.crsum, headless=true) }} | |||
{{ CuneiForms.general_form_tail(vouchsum_form, populate=false) }} | |||
<!-- ... --> | |||
{{ CuneiTables.general_table_block(vat_tb, "max(100vh - 431px, 150px)", | |||
block_width="84vw", ...) }} | |||
{{ CuneiTables.general_table_tail(vat_tb, false) }} | |||
<!-- ... --> | |||
{{ CuneiForms.general_form_head(vatsum_form) }} | |||
{{ CuneiForms.render_blank(vatsum_form, vatsum_form.amtsum, headless=true) }} | |||
{{ CuneiForms.render_blank(vatsum_form, vatsum_form.taxsum, headless=true) }} | |||
{{ CuneiForms.general_form_tail(vatsum_form, populate=false) }} | |||
<!-- ... --> | |||
{{ CuneiTables.general_table_block(wht_tb, | |||
"max(100vh - 431px, 150px)", block_width="84vw", | |||
modal_shift={"ModalTaxIden":"ModalTaxIden2"}, ...) }} | |||
{{ CuneiTables.general_table_tail(wht_tb, false) }} | |||
<!-- ... --> | |||
{{ CuneiForms.general_form_head(whtsum_form) }} | |||
{{ CuneiForms.render_blank(wh, whtsum_form.amtsum, headless=true) }} | |||
{{ CuneiForms.render_blank(whtsum_form, whtsum_form.taxsum, headless=true) }} | |||
{{ CuneiForms.general_form_tail(whtsum_form, populate=false) }} | |||
<!-- ... --> | |||
</div> | |||
</div> | |||
</div> | |||
</div> | |||
{{ CuneiScripts.draw_copymodal(doc_context["mainseq_copy"]) }} | |||
{{ CuneiScripts.draw_attachmodal(doc_context["mainseq_attach"]) }} | |||
{{ CuneiScripts.draw_modaldelmodal(doc_context["mainseq_modaldel"]) }} | |||
{{ CuneiScripts.draw_modalprintmodal(doc_context["mainseq_modalprint"]) }} | |||
{{ CuneiScripts.draw_importmodal(doc_context["mainseq_upload"]) }} | |||
{{ CuneiScripts.draw_schtbs(doc_context["searchers"]) }} | |||
{{ CuneiForms.general_form_tail(head_form, true, pack_del=true) }} | |||
{% endblock content_wgreeter %} | |||
{% block greeter_jvs %} | |||
<!-- ... --> | |||
<!-- DEFINE DOCVERIFY FUNCTIONS --> | |||
function blip_vouch_tab() {...} | |||
function blip_vat_tab() {...} | |||
function blip_wht_tab() {...} | |||
<!-- DEFINE AND LIST POST-POPULATE FUNCTIONS --> | |||
page_post_load_cmds = [..., ..., ...]; | |||
<!-- INITIATE AND BIND DOCUMENT CONTEXT ELEMENTS & EVENTS --> | |||
{% if mass_populate %} | |||
mass_pop_params = {{ mass_populate|safe }}; | |||
bind_mass_populate(); | |||
mass_populate(); | |||
{% endif %} | |||
focus_first(); | |||
{% endblock greeter_jvs %} | |||
</syntaxhighlight> | |||
{{The Tenko Shrine}} | {{The Tenko Shrine}} |
รุ่นแก้ไขปัจจุบันเมื่อ 11:33, 24 กรกฎาคม 2567
While CuneiForms and CuneiTables on each of their own can create functional pages for several purposes, most accounting and transaction records require both to be displayed and manipulated in a meaningful manner. And while it is possible for such pages to be perfectly functional by stringing stad-alone forms and tables together, the numbers of requests and database accesses are unnecessarily high.
In the CuneiFox framework, inter-related forms and tables can be linked together via the use of Document Context. Below are the key functions of Document Context:
- Control page-level permissions and operations.
- Perform a document-lock/unlock upon entering entering/exiting the page's edit mode.
- Consolidate populating request for all forms and tables on the page.
- String together page edit sequence.
Page Sequence
In CuneiFox, modifying a multi-component entry (usually a document) is done in pre-defined steps. This design is to make the process more predictable and, thus, minimize the complexity of data handling behind the scene. The most common pattern of a multi-component page behaves as follows:
- The page is opened first in read-mode. In this mode, modifications to any form fields and table data are disabled.
- To make any modification, the user must enter the edit-mode, where forms and tables are enabled on a step-by-step basis. Entering the edit-mode by clicking the page-level Add or Edit button first allows the user to modify elements in the first page sequence (highlighted yellow).
- The user can proceed to the next sequence by clicking the page-level Proceed button.
- The user can may exit back into the read-mode by:
- Clicking the page-level Cancel button at any step.
- Clicking the page-level Proceed button on the last editing step.
NOTE: Step 3 and 4 can be blocked until certain conditions are met via Document Context key proceed_lock_seq.
Page-level Permission
When dealing with a multi-component record, one cannot assume the permission bit of that record type dictate the actions actually allowed for each sub-component. For example, a user authorized to edit (page-level permission: 3) Accounting Journal is expected to be able to delete (table-level permission: 4) individual debit and credit records as well.
Therefore, it is perfectly acceptable to max out the element-level permissions of all tables and forms on the page (set to 4 or higher), as long as the page-level permission exists to control users' access to different operation. On the server-side, it is also recommended to recheck whether the operation requested from the client-side is allowed. (If the developer follows the common pattern, this check is taken care of via the function process_allowed_action
.)
Beacon & Document Locking
CuneiFox's beacon & document locking system prevents multiple users from modifying the same document simultaneously. In essence, the document locking works as follows:
- Locking a document is done by updating the column
editting
(the misspelling has become intentional, a relic of a past mistake) of the document's master entry (usually the document header) with the username. (See #Page Permission → Document Lock → Allowed Actions below.) - Unlock the document by updating the
editting
column with a blank string (''
). The document unlock happens when:- The user exits from the edit-mode on an existing document. (A newly created master entry counts as an existing document.)
- The document is unlocked temporarily when the browser tab is hidden while the user is in the edit-mode. The document is automatically re-locked when the tab returns to visibility.
Beacon Keys
For the beacon system to properly lock/unlock a document, it must know the document is in the company's database. Beacon keys help point the system to the correct target.
- Javascript-packed: These set of keys indicate the client-side state when the beacon is sent, and thus must be packaged along with the beacon via client-side Javascript. For pages using the usual patterm, these keys are automatically packaged and are accessible on the server-side via either
request.data
orrequest.form['bdata']
.- token (str): The session token. This is used to determine whether the session's user can lock/unlock the document. (For example, a user cannot unlock a document that is locked by another user.)
- mode (str): The type of the beacon sent. This value can take either of the following:
'start'
: The user first enters the edit-mode. If the document already exists, CuneiFox will lock the document.'resume'
: The page resumes edit-mode after automatically exiting it as the user navigates to other tabs or apps. If the document exists, CuneiFox will lock the document.'done'
: The user exits the edit-mode via the page-level Proceed button while being in the final page sequence. CuneiFox unlocks the document.'cancel'
: The user exits the edit-mode via the page-level Cancel button. If the document exists, CuneiFox will unlock the document.
- cur_seq (int): The number representing the page sequence the user is in when the beacon fires (with the first sequence denoted by
0
). This value is not utilized during the lock/unlock step, but can be highly necessary during the document's wrapup algorithms. - seq_count (int): The number of steps in the page sequence.
- cur_mainid (int): The ID of the document's main record (the document's header).
- time_diff (float): The difference between the server time and the session time in seconds. Because of the way CuneiFox stores its data, this value is necessary to pinpoint the database file in need.
- Database Pointer: The variables that points the program to the correct database file. This is usually hard-coded into each specific beacon-handling route or fed via request parameters for a shared route.
- bookcode (str): The name of the database file. In CuneiFox convention, this is usually a 3-lettered string.
- is_gl (str:
'yes'
or'no'
): Whether the target database is an Accounting Journal. (By CuneiFox convention, an accounting journal database name is prefixed withGL_
for exampleGL_GL0
.) - datatype (str:
'table'
,'data'
, or'report'
): The data type (location) of the database file.
- Other Keys: These keys are quite rare and are usually explicitly hard-coded in each specific Python route when needed.
- skip_lock (bool): Used exclusively by the Python function
process_doclock
to make it return immediately without performing document locking/unlocking.
- skip_lock (bool): Used exclusively by the Python function
Beacon Routine Functions
The 2 groups of beacon keys discussed above can be converted into a dict form for ease of use via the Python function process_beacon_args
.
process_beacon_args(internal_args, request)
Parameters |
|
Returns |
|
Notes |
The function throws a 461 exception (Session-time Error) if the beacon value time_diff is invalid or does not agree with the session time. |
The process of locking/unlocking a document can also be automated via another function process_doclock
.
process_doclock(beacon_args, module_name, perm_name, headmodel=None, paired_gl=None)
Parameters |
|
Returns |
|
Page Permission → Document Lock → Allowed Actions
In a sense, the actions that a user can perform on a page are dictated by the user's permission. However, in the messy real world, permission bits are no the only factor at play here. In CuneiFox, allowed actions are determined as follows:
- When a user initiates a page-level action, CuneiFox first determines whether the user is authorized to do so (via a permission bit check). Then,
- For an edit operation, the program checks whether the document is locked by another user. If not, the document lock is applied under the current user's name.
- For a delete operation, the user is only allowed to proceed if the document is not locked by another user.
- For an add operation, the program automatically proceeds. The document lock is to be applied automatically once the document header is committed to database.
- When a user submits a CuneiForm (a stand-alone form, an in-line form, a CuneiTable's full-form), the recommended template has CuneiFox re-interpret the permission bit and the document lock state into allowed action dictionary via Python functions
check_doclock
andprocess_allowed_action
. The form submission is only processed if the operation is allowed.
check_doclock(datatype, bookcode, headmodel, form, field,
doc_perm_bit, head_id_col="id", paired_gl=None)
Description | Based on the current lock-state of a particular document, returns the dict of actions the user is allowed to perform at a header level. |
Parameters |
|
Returns |
|
Notes | The function throws a 491 exception (Frozen-data Error) or 492 exception (Posted-data Error) if the month's data is frozen or locked respectively. |
process_allowed_action(head_action_dic, is_head)
Description | Interprets the head_action_dic (returned from check_doclock ) and whether the current form is for the document header, returns a new allowed_action dict that is really applicable to the current form.
By CuneiFox standard, all actions is allowed for the sub-entries if the user holds the edit permission at the header level (including the temporary edit permission granted upon document addition.) |
Parameters |
|
Returns |
|
Define a Document Context
Document Context is a dict fed to the Flask render_template
function. Most of the keys in the dict are handled (with default values assigned, if not given) by a separate Python function prep_mainseq
. We shall begin with these so-called Main Sequence keys, then we will discuss a few additional keys.
Main Sequence Keys
Page Permission & Sequence Definition
- mainseq ([[CuneiForm/CuneiTable,],]: defaults to
[]
): The list of page sequence specifications. Each member is a list of CuneiForms and/or CuneiTables belonging to that specific sequence. - mainseq_name ([[str,],]: defaults to
[]
): Basically mainseq, but repacked with _id attributes (str) of each CuneiForm/CuneiTable in place of the object itself. (This value is automatically created can only used for ease of internal references.) - page_perm (int: defaults to
1
): Page-level permission value.- 0: Not readable. (Usually users with this permission level should be prevented from reaching the page at all. DO NOT rely on this permission bit as a security measure,)
- 1: Read-only.
- 2: Add. (Allows 'add' type actions: add and copy.)
- 3: Add + Edit.
- 4: Add + Edit + Delete.
- ≥5: Add + Edit + Delete + Import.
- proceed_lock_seq ([[str,],]: defaults to
[]
): List of document verification items that should prevent proceeding/canceling away from each sequence. - searchers [CuneiTable,]: defaults to
[]
: List of search tables.
Basic Actions
- mainseq_add (str: defaults to
'plain'
): Action type when the page-level Add button is clicked.'newmaster'
: The button triggers the addition of a new master entry. (This setting assumes the existence of a Master Form and certain Mass Populate setting.)- Any other string: The button triggers the function
this_page_add(event)
. This function must be custom written for each page that needs it.
- mainseq_del (str: defaults to
'plain'
): Action type when the page-level Delete button is clicked.'delmaster'
: The button triggers the prompt for the deletion of the current master entry. (This setting assumes the existence of a Master Form and certain Mass Populate setting.)- Any other string: The button triggers the function
this_page_delete(event)
. This function must be custom written for each page that needs it.
- mainseq_proceed ([str,]: defaults to
[]
): Each sequence's action type when the page-level Proceed button is clicked.'submit'
: Submit the first CuneiForm in the current page sequence. Note that this proceeding mode DOES NOT transmit a 'cancel' beacon. So, if the final sequence of the page has this proceed mode, the duty of unlocking the document falls on the submission route of the final form.'beacon'
: Beam a 'proceed' beacon to the server to signal a page sequence change. (For the final page sequence, a 'cancel' beacon is sent instead to signal checking out of the page's edit mode.)
- mainseq_search (str: defaults to
False
): Field name of the Master Form used for page-level search. - mainseq_schprmpt (str: defaults to
'Document Number'
): Prompt text for page-level search.
Request-based Actions
- mainseq_beacon (url_for_string: defaults to
False
): Route for beacon transmission. - mainseq_print (url_for_string: defaults to
False
): Route for print request. - mainseq_qr (url_for_string: defaults to
False
): Route for Payment QR-code request. - mainseq_vouch (url_for_string: defaults to
False
): Route to corresponding Accounting Journal record. (Usually directed throughcunei_gl.direct_to_vouch
.) - mainseq_export (url_for_string: defaults to
False
): Route to export request.
Form-based Actions
- mainseq_modaldel (CuneiForm: defaults to
False
): If defined, right-clicking on the page-level Delete button triggers a CuneiModal containing this form. The standard Mass Deletion Form is pre-defined at cuneifox.base.main.forms.StdModalDelForm. - mainseq_copy (CuneiForm: defaults to
False
): If defined, clicking on the page-level Copy button triggers a CuneiModal containing this form. The standard Document Copy Form is pre-defined at cuneifox.base.main.forms.StdCopyForm. - mainseq_attach (CuneiForm: defaults to
False
): If defined, clicking on the page-level Attach button triggers a CuneiModal containing this form. The standard Document Attach Form is pre-defined at cuneifox.base.main.forms.StdAttachForm. - mainseq_modalprint (CuneiForm: defaults to
False
): If defined, right-clicking on the page-level Print button triggers a CuneiModal containing this form. The standard Custom Printing Form is pre-defined at cuneifox.base.main.forms.StdModalPrintForm. - mainseq_upload (CuneiForm: defaults to
False
): If defined, clicking on the page-level Import button triggers a CuneiModal containing this form. The standard Document Import Form is pre-defined at cuneifox.base.main.forms.StdImportForm.
# Example from Accounting Journal (w/ Input VAT) page
# MAIN SEQUENCE FORMS/TABLES
head_form = VoucherHeadForm.init_form(prefix="VoucherHeadForm",
gen_del="cunei_gl", sequence_bound=True,
populate_id=["id"], populate_suppress=["grab"],
post_route=url_for("<head_submit_route>"), ...)
vouch_tb = VouchBodyTable(prefix="VouchBodyTable", editable=True, perm_bit=4,
populate_route=url_for("<vouch_default_val_request_route>"),
populate_id=["master.id"], populate_suppress=["qsch", "free_grab"],
post_route=url_for("<vouch_submit_route>"), ...)
vouchsum_form = VoucherDetailSum.init_form(prefix="VoucherDetailSum", ...)
vat_tb = VouchVatTable(prefix="VouchVatTable", editable=True, perm_bit=4,
populate_route=url_for("<vat_default_val_request_route>"),
populate_id=["master.id", "#master.section_stid",
"#master.docdate", "#master.desc"],
populate_suppress=["qsch", "free_grab"],
post_route=url_for("<vat_submit_route>"), ...)
vatsum_form = VatDetailSum.init_form(prefix="VatDetailSum", ...)
wht_tb = VouchWhtTable(prefix="VouchWhtTable", editable=True, perm_bit=4,
populate_route=url_for("<wht_default_val_request_route>"),
populate_id=["master.id", "#master.section_stid",
"#master.docdate", "#master.desc"],
populate_suppress=["qsch", "free_grab"],
post_route=url_for("<wht_submit_route>"), ...)
whtsum_form = WhtDetailSum.init_form(prefix="WhtDetailSum", ...)
# SEARCH TABLES
# (Note that most search tables use unified 'Search Table with a Stand-alone Page'.
# See corresponding section under CuneiTable page for the template.)
section_tb = SectionTable(prefix="SectionTable", perm_bit=<user_perm_for_section>,
populate_route=url_for("cunei_gl.section", fetch="yes"),
post_route=url_for("cunei_gl.section"),
in_modal="Modal0", modal_head=lazy_gettext("Choose Section"), ...)
acccode_tb = AccCodeTable(prefix="AccCodeTable", perm_bit=<user_perm_for_acccode>,
populate_route=url_for("cunei_gl.acccode", fetch="yes"),
post_route=url_for("cunei_gl.acccode"),
in_modal="Modal1", modal_head=lazy_gettext("Choose Account Code"), ...)
...
# START CREATING DOCUMENT CONTEXT
mainseq = [[head_form],
[vouch_tb, vat_tb, wht_tb, vouchsum_form, vatsum_form, whtsum_form]]
attach_form = StdAttachForm.gen_attach_form(post_route=url_for("<attach_submit_route>"),
populate_route=url_for("<attach_fetch_route>"))
modalprint_form = StdModalPrintForm.gen_modalprint_form(post_route=url_for("<custom_prn_submit_route>"))
modaldel_form = StdModalDelForm.gen_modaldel_form(post_route=url_for("<mass_del_submit_route>"))
modalcopy_form = StdCopyForm.gen_copy_form(url_for("<copy_submit_route>"), ...),
modalimport_form = StdImportForm.gen_import_form(url_for("<import_route>"))
# USE 'pack_mainseq' TO FILL UNSPECIFIED KEYS WITH DEFAULT VALUES.
mainseq_args = pack_mainseq(mainseq=mainseq,
mainseq_add="newmaster",
mainseq_del="delmaster",
page_perm=<user_perm_for_the_page>,
mainseq_proceed=["submit", "beacon"],
proceed_lock_seq=[[], ["incomp"]],
mainseq_search="docno",
mainseq_print=url_for("<print_route>"),
mainseq_export=url_for("<export_route>"),
mainseq_beacon=url_for("<beacon_route>"),
mainseq_copy=modalcopy_form,
mainseq_attach=attach_form,
mainseq_modalprint=modalprint_form,
mainseq_modaldel=modaldel_form,
mainseq_upload=modalimport_form,
searchers=[section_tb, acccode_tb, ...])
Additional Document Context Keys
- btn_set (bool): Whether the page button sets should be rendered.
- doccount (bool): Whether the document counter elements should be rendered.
- docverify (dict): The specifications for the document verification blocks. Each key-value pair corresponds to a single block:
- The key is a string used for internal referencing.
- The value is a list ([str, str, str (optional)]):
- A short human-readable description of the verification. This value is the popover text when the mouse cursor hovers above the block.
- Pill text, a highly abbreviated shorthand string that appears on the block at all time.
- (Optional) The name of the function to run when the verification is not an all-out pass. Usually the function serves to highlight the elements where attention is needed.
# Example from Accounting Journal (w/ Input VAT) page
doc_context = {"btn_set": True,
"doccount": True,
"docverify": {"incomp": ["INCOMPLETE", "INCOMP", "blip_vouch_tab"],
"iwht": ["INPUT WITHHOLDING TAX", "IN-WHT", "blip_wht_tab"],
"owht": ["OUTPUT WITHHOLDING TAX", "WHT", "blip_wht_tab"],
"bvat": ["TAX INVOICE", "VAT", "blip_vat_tab"],
"editting": ["EDITTING", "EDIT"]},
**mainseq_args)
Mass Populate Route
See more detailed discussion on Mass Populate under Page.
The mass populate route is not defined as a part of the Document Context and is fed directly to the render_template
function. Although its use is not limited to multi-component pages, it has enough exclusivity to warrant some discussion here.
Most document pages within CuneiFox system have their mass_populate set as:
mass_populate = ["<mass_populate_route>",
["master.id", "window.pagenav"]]
- The trigger
'master.id'
refers to the (usually hidden) id field of the master (document header) form. - The trigger
'window.pagenav'
refers to the hidden<div>
named'pagenav'
.
The hidden element 'pagenav'
is a very common trigger for mass populating routine. When set, the pagenav trigger works as follows:
- The
documentElement
and Arrow buttons in the navigation button set listen to certainkeydown
orclick
events, then change theinnerHTML
ofpagenav
accordingly: - A
change
event is then triggered for the pagenav element. This, in turn, triggers the page's mass population. - The mass population routine involves collecting values from all the triggers to send along side the request. These values, including the
innerHTML
of pagenav, is interpreted on the server-side.
Fit the Context on an HTML Page
Sequence Indicator
Page-level Post Populate Functions
Commonly-used HTML Blocks
It cannot be denied that CuneiFox templates form a rather comlex system; setting up a new HTML page, especially one with multiple inter-dependent pieces, from scratch can be tedious. In this section, we assume the use of CuneiFox's own layout.html
as the starting point.
WAIT
In practice, however, one might find it a little easier to start with greeter_slim.html
instead. (See #HTML Page Pattern.)
Useful Patterns
Standard Forms
Unified Render-Fetch-Submit Route
@<blueprint>.route("<unified_route>", methods=["GET", "POST"])
@login_required
def <function_name>(*args):
# CHECK FOR MODULE ACTIVATION
check_modact_stat(modcode=pr_moddata["code"], mode="menu", response="abort")
# PERFORM PERMISSION CHECK
# ...
# SCREEN OUT THE PAGE-LEVEL SEARCH REQUEST FIRST
page_search_kw = request.args.get("page_search", None)
if not(page_search_kw is None):
goal_id = grab_id(...)
if goal_id is None:
return redirect(url_for("<unified_route>"))
else:
return redirect(url_for("<unified_route>", id=goal_id))
# PREPARE THE PAGE'S FORMS AND TABLES.
# (Depending on the type of the request, seach tables may or may not be created here.)
# Pack those along with other 'doc_context'.
# ...
# ...
# ...
# DISTINGUISH THE REQUEST INTO ONE OF FETCH, SUBMIT, OR PAGE-GET
# ...
### Cases of fetch and submit usually comes with a form/table pointer.
### This pointer should be encoded in the populate routes of the object/page.
fetch_tbfm = request.args.get("tbfm") if request.args.get("tbfm") else None
if <is_fetch>:
identifiers = json.loads(request.form.get("identifier"))
if fetch_tbfm == "_all": # This usually marks a MASS_POPULATE request.
thisdoc, doccount, flash = grab_whole_doc(...)
if thisdoc[head_form._id] is None: # No document found
# Usually just fill all fields of the header with None
thisdoc[head_form._id] = {col:None for col in head_form.seq}
elif thisdoc[head_form._id] == "ncol": # Default for new document requested
thisdoc[head_form._id] = dict(...)
else: # Found a document
thisdoc[head_form._id] = {col:getattr(thisdoc[head_form._id], col, None) \
for col in head_form.seq}
# Modify as needed.
# ...
# It should be noted here that a database fetch returns CuneiModel instances.
# These instances need to be repackaged into DICT for forms and LIST-OF-LIST for tables.
# ...
# temp_dicts = [a.m2d(int_key=["doc"]) for a in thisdoc[<tb>._id]]
# thisdoc[<tb>._id] = [[a.get(col, None) for col in <tb>.cnames] for a in temp_dicts]
# ...
# Some forms/tables are filled with freshly calculated values. Pack those here.
# ...
# DETERMINE DOCUMENT VERIFICATION STATE and its implication the page-level actions.
docverify = dict(...)
if doccount[1] <= 0 or <some_other_conditions>:
editdisable = ["edit", "del"]
else:
editdisable = []
return jsonify({"data":thisdoc, "doccount":doccount, "docverify":docverify,
"flash":flash, "editdisable":editdisable})
# Besides MASS_POPULATE (and properly suppresses), multi-component pages
# almost exclusively request default data set for each table.
# ** A table's default is returned in a LIST form.
elif fetch_tbfm == "<table_marker>":
if request.form.get("mode") == "ncol":
return jsonify({"data":[...]})
elif fetch_tbfm == "<another_table_marker>":
if request.form.get("mode") == "ncol":
return jsonify({"data":[...]})
elif issubmit:
# CHECK DOCUMENT'S LOCK STATE AND ALLOWED ACTIONS
head_action_dic = check_doclock(...)
if isinstance(head_action_dic, int):
return jsonify(message='Input 403 message here'), head_action_dic
allowed_action = process_allowed_action(head_action_dic, fetch_tbfm=="head")
if allowed_action == 409:
return jsonify(message='Input 409 message here'), 409
# PERFORM FORM VALIDATION AND DATABASE COMMIT VIA 'GEN_DBCOMMIT_RESP'
db_mod_code, new_entry, resp_dict = gen_dbcommit_resp(...)
if resp_dict == 403:
abort(403)
# IF 'NEW_ENTRY' IS PRESENT, THE CHANGE WAS COMMITTED TO DATABASE.
# Modify 'resp_dict' as needed. Only necessary modifications are illustrated below.
if not(new_entry is None):
# THE WHOLE DOCUMENT MIGHT OR MIGHT NOT BE RE-FETCHED.
# _, fetched_doc = prefetch_all_docs(...)
if fetch_tbfm == "head":
if db_mod_code == 4: # This is the case of HEADER DELETION.
resp_dict["repop"] = gen_form_skipper(head_form, {"id":-2})
resp_dict["del_master"] = True
else:
if db_mod_code == 2: # This is the case of NEWLY ADDED HEADER.
resp_dict["repop"] = gen_form_skipper(head_form, {"id":new_entry.get("id", None)})
elif fetch_tbfm == "<table_marker>":
# Data committed to sub-tables is repackaged for the client.
if isinstance(db_mod_code, int) and db_mod_code < 4:
resp_dict["entry"] = [new_entry.get(col, None) for col in bill_tb.cnames]
# Most sub-table commits require re-calculations of sum values shown
# on the client side as well.
resp_dict["crossfill"] = {<table_sum_form>._id:dict(...)}
# POST-COMMIT ROUTINE GOES HERE.
# ...
# ...
return jsonify(resp_dict)
# NOW ONLY PAGE-GET REQUEST REMAINS!
# ...
# This line works solo and along-side the page-level search from above.
head_form.id.data = request.args.get("id", "")
# ...
title = lazy_gettext("...")
return render_template("<template_file_name>", title=title, greeter=title,
mass_populate=..., pin_args=[...], perm_to_show=cur_perm,
doc_context=dict(btn_set=True, doccount=True, docverify=dict(...),
**mainseq_args))
Beacon Route
@<blueprint>.route("<beacon_route>", methods=["POST"])
def <function_name>(*args):
# UNPACK BEACON KEYS/VALUES
beacon_args = process_beacon_args(...)
# ...
# PROCESS DOCLOCK. Aborting is acceptable.
# ...
lock_stat = process_doclock(beacon_args, ...)
if lock_stat == 403:
abort(403)
# (OPTIONAL) ADDITIONAL ROUTINES FOR THIS DOCUMENT TYPE
# ...
# ...
# FINALLY, when all is done successfully,
# return something (a string is acceptable).
return "YES"
Copy Route
@<blueprint>.route("<copy_route>", methods=["GET", "POST"])
def <function_name>(*args):
# RE-CREATE THE COPY FORM AND UNPACK COPY PARAMETERS
# ...
cp_form = StdCopyForm.gen_copy_form(...)
# VIA PREP_COPY, GET THE FULL ENTRY (WITH SUB-ENTRIES) OF THE ORIGINAL DOC.
# THE FUNCTION ALSO INTERPRETS COPY PARAMETERS FROM THE FORM AS WELL.
return_dict, orig_doc, copy_params = prep_copy(cp_form, ...)
if orig_doc is None:
return jsonify(return_dict)
# MANIPULATE THE DOCUMENT INTO THE NEWLY COPIED FORM.
# (Note that not all values are copied verbatim.)
new_docdate = copy_params["docdate"]
# ...
# CREATE A COPY OF THE DOCUMENT VIA CREATE_DOC_COPY
new_head = create_doc_copy(cp_form, copy_params, ...)
if new_head is None:
return_dict["err"] = {"docno":[lazy_gettext("Copy error!")]}
return jsonify(return_dict)
# CHANGE THE SESSION DATE-TIME, THEN PERFORM ANY NEEDED POST-COPY ROUTINE
# (e.g. debt history, inventory management, etc.)
is_success, _, timediff = change_datetime(form=None, date_obj=new_head.docdate)
if is_success:
session["time_diff"] = timediff
# ...
# FINALLY, PREPARE THE REDIRECT ROUTE (usually the same route with the new
# docid specified) AND RETURN
return_dict["redirect"] = url_for("<redirect_route>", id=new_head.id, ...)
return jsonify(return_dict)
Unified Attach Route
@<blueprint>.route("<attach_route>", methods=["GET", "POST"])
def <function_name>(*args):
# CHECK FOR MODULE ACTIVATION
check_modact_stat(...)
# CONDUCT PERMISSION CHECK
# ...
# CREATE AN ATTACH FORM AND PROCESS THE SUBMITTED RESULT
at_form = StdAttachForm.gen_attach_form()
return process_doc_file(at_form, ...)
Document Mass-delete Route
@<blueprint>.route("<massdel_route>", methods=["GET", "POST"])
def <function_name>(*args):
@copy_current_request_context
def <proxy_function>(*proxy_args):
try:
has_err = doc_mass_delete(job_token=job_token, ...)
except Exception:
update_task_db(job_token, new_stat="killed", ...)
# CHECK FOR MODULE ACTIVATION
check_modact_stat(...)
# PERFORM PERMISSION CHECK
# ...
# DETERMINE THE LIST OF DOCUMENTS TO BE DELETED
# (This example illustrates this step via 'std_modaldel_to_docno_list').
resus_token = request.args.get("resus_token", None)
immediate_return, return_dict, doccount, thesedocs = \
StdModalDelForm.std_modaldel_to_docno_list(..., resus_token=resus_token)
if not(immediate_return): # Form validation succeeds
job_token = gen_random_token(10) if resus_token is None else resus_token
new_task = init_task_db(job_token, 1, "request", suppress_request=False)
if new_task is None:
raise current_app.custom_exceptions.default_exceptions[410]()
if new_task.status_str != "queue":
job_resuscitation = False
threading.Thread(target=<proxy_function>, name=job_token, args=[...]).start()
if not(resus_token is None):
return "YES"
else:
job_resuscitation = url_for("<massdel_route>", resus_token=job_token, ...)
update_task_db(job_token,
further_action=pjson.dumps({"docids":[mem.id for mem in thesedocs]}))
return jsonify({"redirect": url_for("main.delete_progress", job_token=job_token,
job_resuscitation=job_resuscitation, ...)})
return jsonify(return_dict)
Unified Print & QR Code Route
@<blueprint>.route("<print_route>", methods=["GET", "POST"])
def <function_name>(*args):
@copy_current_request_context
def <proxy_function>(job_token, doc_objs, ...):
# ...
print_in_thread(job_token, doc_objs, pseudo_mapper={...}, ...)
def <doc_modify_function>(doc_obj):
method_dict = dict(paymethods)
for r in doc_obj.route:
r.paymethod = method_dict.get(r.paymethod, r.paymethod)
# CHECK FOR MODULE ACTIVATION
check_modact_stat(...)
# THIS FIRST BLOCK IS FOR PRINTING VIA THE PRINT-MODAL
if request.args.get("modalprint", "no") == "yes":
resus_token = request.args.get("resus_token", None)
immediate_return, return_dict, chosen_template, make_pdfa3, doccount, thesedocs = \
StdModalPrintForm.std_modalprint_to_docno_list(..., resus_token=resus_token)
if not(immediate_return): # Form validation succeeds
job_token = gen_random_token(10) if resus_token is None else resus_token
new_task = init_task_db(job_token, 1, "request", suppress_request=False)
if new_task is None:
raise current_app.custom_exceptions.default_exceptions[410]()
if new_task.status_str != "queue":
for d in thesedocs:
<doc_modify_function>(d)
job_resuscitation = False
threading.Thread(target=<proxy_function>, name=job_token, args=[...]).start()
if not(resus_token is None):
return "YES"
else:
job_resuscitation = url_for("<print_route>", resus_token=job_token, ...)
update_task_db(job_token,
further_action=pjson.dumps({"chosen_template":chosen_template,
"make_pdfa3":make_pdfa3,
"docids":[mem.id for mem in thesedocs]}))
return_dict["new_tab"] = url_for("main.print_pdf", job_token=job_token,
final_name="{}_out.pdf".format(current_user.code),
job_resuscitation=job_resuscitation)
return jsonify(return_dict)
# BELOW THIS POINT IS THE NORMAL PRINT/QR ROUTINE
try:
docid = int(request.args.get("master.id"))
except (ValueError, TypeError, KeyError):
abort(404)
_, thisdoc = prefetch_all_docs(...)
if thisdoc is None:
abort(404)
if request.args.get("qr") == "yes":
# ...
qr_file = gen_qr(...)
if qr_file == 404:
abort(404)
return send_file(qr_file)
<doc_modify_function>(thisdoc)
pdf_path = replace_printsvg(...)
if pdf_path is None:
abort(404)
return render_template("print_pdf.html", pdf_path=pdf_path)
Document Export Route
@<blueprint>.route("<export_route>", methods=["GET", "POST"])
def <function_name>(*args):
# CHECK FOR MODULE ACTIVATION
check_modact_stat(modcode=pr_moddata["code"], mode="menu", response="abort")
# FETCH THE DOCUMENTS & MODIFY THEM IF NEEDED
_, thesedocs = prefetch_all_docs(...)
thesedocs = [] if thesedocs is None else thesedocs
# ...
# EXPORT VIA 'GEN_SPREADSHEET'
ss_path = gen_spreadsheet(...)
if ss_path is None:
abort(404)
return render_template("serve_file.html", file_path=ss_path, dl_name="cunei_export.xlsx")
Document Import Route
@<blueprint>.route("<import_route>", methods=["GET", "POST"])
def <function_name>(*args):
@copy_current_request_context
def <proxy_function>(job_token, imported_file, ...):
try:
has_err = docimport_to_db(imported_file, ..., job_token=job_token)
except Exception as e:
update_task_db(job_token, new_stat="killed", ...)
remove(imported_file)
# CHECK FOR MODULE ACTIVATION
check_modact_stat(modcode=iv_moddata["code"], mode="menu", response="abort")
# PERFORM PERMISSION CHECK
# ...
# RESUS_TOKEN IS PRESENT IN CASE OF A QUEUED OPERATION:
# Either read the needed data from the form or read from stashed info.
resus_token = request.args.get("resus_token", None)
if resus_token is None:
if not(import_form.validate_on_submit()):
return jsonify({"err": import_form.errors})
file_path = prep_imported_file(import_form.filename)
job_token = gen_random_token(10)
else:
file_path = read_resus_import_file(resus_token, will_abort=True)
job_token = resus_token
# ATTEMPT TO INITIATE THE OPERATION
new_task = init_task_db(job_token, 1, "request", suppress_request=False)
if new_task is None:
raise current_app.custom_exceptions.default_exceptions[410]()
# IF THE OPERATION CAN START IMMEDIATELY (NOT QUEUED): Run the routine.
# IF NOT: Stash necessary info (esp. job_token) for future retrieval.
if new_task.status_str != "queue":
job_resuscitation = False
threading.Thread(target=<proxy_function>, name=job_token, args=[...]).start()
if not(resus_token is None):
return "YES"
else:
job_resuscitation = url_for("<import_route>", resus_token=job_token, ...)
update_task_db(job_token, further_action=pjson.dumps({"file_path":file_path}))
return jsonify({"redirect": url_for("main.import_progress", job_token=job_token,
job_resuscitation=job_resuscitation, ...)})
HTML Page Pattern
<!-- EXAMPLE FROM ACCOUNTING JOURNAL PAGE -->
{% import "cunei_forms.html" as CuneiForms %}
{% import "cunei_tables.html" as CuneiTables with context %}
{% import "cunei_modals.html" as CuneiModals with context %}
{% import "cunei_scripts.html" as CuneiScripts %}
{% import "cunei_icons.html" as CuneiIcons %}
<!-- MANY DOCUMENT PAGES USE 'GREETER_SLIM' AS THE BASE. -->
{% extends "greeter_slim.html" %}
<!-- SET SOME NAMES FOR EASY REFERENCES. -->
{% set head_form = doc_context["mainseq"][0][0] %}
{% set vouch_tb = doc_context["mainseq"][1][0] %}
{% set vat_tb = doc_context["mainseq"][1][1] %}
{% set wht_tb = doc_context["mainseq"][1][2] %}
{% set vouchsum_form = doc_context["mainseq"][1][3] %}
{% set vatsum_form = doc_context["mainseq"][1][4] %}
{% set whtsum_form = doc_context["mainseq"][1][5] %}
{% block header %}
<h3>{{ greeter }}</h3>
{% endblock header %}
{% block content_wgreeter %}
{{ CuneiForms.general_form_head(head_form, is_master=true) }}
<div class="col-sm-11">
<div class="card border-dark mb-3" style="max-width:100%;">
<div class="card-body py-0">
<div id="seq_indi_0" class="row align-items-start pt-3">
<!-- DRAW HEADER FORM HERE -->
{{ CuneiForms.render_blank(head_form, head_form.id, hidden=true) }}
<!-- ... -->
<!-- ... -->
{{ CuneiForms.render_submit(head_form, head_form.submit, hidden=true) }}
{{ CuneiForms.render_check(head_form, head_form.is_del, hidden=true) }}
{{ CuneiForms.render_submit(head_form, head_form.del_submit, hidden=true,
class="danger", confirm_first=head_form.confirm_first) }}
</div>
<div id="seq_indi_1" class="row align-items-center mx-0 pt-1 pb-3">
<!-- DRAW BODY TABLES AND FORMS HERE -->
<!-- ... -->
{{ CuneiTables.general_table_block(vouch_tb, "max(100vh - 431px, 150px)",
block_width="84vw", ...) }}
{{ CuneiTables.general_table_tail(vouch_tb, false) }}
<!-- ... -->
{{ CuneiForms.general_form_head(vouchsum_form) }}
{{ CuneiForms.render_blank(vouchsum_form, vouchsum_form.drsum, headless=true) }}
{{ CuneiForms.render_blank(vouchsum_form, vouchsum_form.crsum, headless=true) }}
{{ CuneiForms.general_form_tail(vouchsum_form, populate=false) }}
<!-- ... -->
{{ CuneiTables.general_table_block(vat_tb, "max(100vh - 431px, 150px)",
block_width="84vw", ...) }}
{{ CuneiTables.general_table_tail(vat_tb, false) }}
<!-- ... -->
{{ CuneiForms.general_form_head(vatsum_form) }}
{{ CuneiForms.render_blank(vatsum_form, vatsum_form.amtsum, headless=true) }}
{{ CuneiForms.render_blank(vatsum_form, vatsum_form.taxsum, headless=true) }}
{{ CuneiForms.general_form_tail(vatsum_form, populate=false) }}
<!-- ... -->
{{ CuneiTables.general_table_block(wht_tb,
"max(100vh - 431px, 150px)", block_width="84vw",
modal_shift={"ModalTaxIden":"ModalTaxIden2"}, ...) }}
{{ CuneiTables.general_table_tail(wht_tb, false) }}
<!-- ... -->
{{ CuneiForms.general_form_head(whtsum_form) }}
{{ CuneiForms.render_blank(wh, whtsum_form.amtsum, headless=true) }}
{{ CuneiForms.render_blank(whtsum_form, whtsum_form.taxsum, headless=true) }}
{{ CuneiForms.general_form_tail(whtsum_form, populate=false) }}
<!-- ... -->
</div>
</div>
</div>
</div>
{{ CuneiScripts.draw_copymodal(doc_context["mainseq_copy"]) }}
{{ CuneiScripts.draw_attachmodal(doc_context["mainseq_attach"]) }}
{{ CuneiScripts.draw_modaldelmodal(doc_context["mainseq_modaldel"]) }}
{{ CuneiScripts.draw_modalprintmodal(doc_context["mainseq_modalprint"]) }}
{{ CuneiScripts.draw_importmodal(doc_context["mainseq_upload"]) }}
{{ CuneiScripts.draw_schtbs(doc_context["searchers"]) }}
{{ CuneiForms.general_form_tail(head_form, true, pack_del=true) }}
{% endblock content_wgreeter %}
{% block greeter_jvs %}
<!-- ... -->
<!-- DEFINE DOCVERIFY FUNCTIONS -->
function blip_vouch_tab() {...}
function blip_vat_tab() {...}
function blip_wht_tab() {...}
<!-- DEFINE AND LIST POST-POPULATE FUNCTIONS -->
page_post_load_cmds = [..., ..., ...];
<!-- INITIATE AND BIND DOCUMENT CONTEXT ELEMENTS & EVENTS -->
{% if mass_populate %}
mass_pop_params = {{ mass_populate|safe }};
bind_mass_populate();
mass_populate();
{% endif %}
focus_first();
{% endblock greeter_jvs %}