ผลต่างระหว่างรุ่นของ "CuneiTable"
(ไม่แสดง 13 รุ่นระหว่างกลางโดยผู้ใช้คนเดียวกัน) | |||
บรรทัดที่ 1: | บรรทัดที่ 1: | ||
CuneiTable is designed specifically to enable in-line data entry. Design-wise, it is more limited than CuneiForm since design flexibility is not highly expected from table- | CuneiTable is designed specifically to enable in-line data entry. Design-wise, it is more limited than CuneiForm since design flexibility is not highly expected from table-type objects. In essence, CuneiTable is a wrapper for the accompanying CuneiForm, whether that form be the in-line type or the full expansion type. | ||
== Define == | == Define == | ||
บรรทัดที่ 598: | บรรทัดที่ 598: | ||
|} | |} | ||
==== The Working of General Table Tail ==== | |||
The workings of this macro can be thus summarized: | The workings of this macro can be thus summarized: | ||
# '''Run the ''general_form_tail'' for the table's in-line form or full-form. | # '''Run the ''general_form_tail'' for the table's in-line form or full-form. | ||
บรรทัดที่ 896: | บรรทัดที่ 897: | ||
== Notable Sub-routines == | == Notable Sub-routines == | ||
This section aims to give a little insight into some CuneiTable's notable sub-routines. | |||
=== Live Search === | === Live Search === | ||
Live search is among the most welcomed feature of CuneiTable. Its inner working is not too complicated, relying mainly on a brute-force approach. | |||
==== The Illusion of "Live" Search ==== | |||
Being quite resource- and time-intensive, there is a need to avoid unnecessary live-search sequences. At the time of writing, CuneiTable achieves this by putting a '''700ms delay''' between the most recent keystroke (in the table's search box) and the actual searching. This delay is likely here to stay, but the length of the time is subject to adjustment. | |||
==== Keyword Handling ==== | |||
At the time of writing, CuneiTable's live search handles keywords in the following manner: | |||
* Unless enclosed within a pair of '''double-quotation marks (")''', strings separated by '''Spaces''' are treated as multiple keywords. | |||
* Keywords are case-insensitive. | |||
* Valid live search results are entries that contain '''all keywords''' within their '''visible columns''', but not necessarily in any single column. | |||
* Values in columns typed as {{code|int}}, {{code|acc}}, {{code|qty}}, {{code|amt}}, or {{code|pct}} are searched in their unformatted forms. | |||
* ''(This stems from CuneiTable's lookup method)'' One leading space in a keyword can also be interpreted as '''starting-with'''. | |||
* ''(This stems from CuneiTable's lookup method)'' One trailing space in a keyword can also be interpreted as '''ending-with'''. | |||
=== Selection & Table Tracking === | |||
==== As a Part of Search-and-fill Routine ==== | |||
During the search-and-fill routine, a CuneiTable's role is as follows: | |||
# The search table population routine is triggered when the corresponding search modal is explicitly triggered or when the ''quick-search'' routine fails to retrieve exactly one entry. The search modal (a [[CuneiModal]]) is shown as a part of the table population process. | |||
# The user makes the desired selection. ''(See the dedicated [[#Selection]] sub-section below for details.)'' | |||
# Upon confirmation (via {{key press|Enter}} or the modal's button), the search table requests the full result from the server using the {{code|lang=javascript|'sel'}} fetch mode. ''(The table itself only holds values of the columns defined in the [[#Define|table's class]]. The full result, on the other hand, holds full information of the selected entries.)'' | |||
# The table stores the full result in the its '''selected_entries''' attribute, then hands the control of the sequence to its modal. | |||
==== Selection ==== | |||
CuneiTables remember their selection states by storing '''row indices''' of the selected entries within their '''selections''' attribute. The list below details some notable behaviours: | |||
* '''For single-selection tables:''' | |||
** Navigating to an entry (via keyboard or mouse click) automatically implies its selection. | |||
** The record counter shows the index of the selected entry. | |||
* '''For multi-selection tables:''' | |||
** Navigating to an entry via keyboard '''DOES NOT''' automatically select the entry. | |||
** Clicking on an unselected entry selects it, and clicking on a selected entry deselects it. | |||
** The record counter shows the number of entries being selected. | |||
** It is possible to select/deselect consecutive entries. Note that this feature only works with entries actively shown (not hidden by live search). To select/deselect consecutive entries: | |||
**# Click the topmost/bottommost entry to select/deselect. | |||
**# Click the bottommost/topmost entry while holding {{key press|Shift}}. | |||
==== Table Tracking ==== | |||
Table tracking can be seen as a fast-and-loose version of the search-and-fill routine. While forms that employ search-and-fill have access to the full entries re-retrieved from the server once the selections are confirmed, forms with table-tracking fields only have the data immediately in the table. This eliminates the need of additional requests and creates more streamlined user-experience, at the expense of up-to-date results. | |||
=== In-line Editing === | |||
==== Form Activation ==== | |||
In CuneiFox, adding/editing a table entry triggers the following sequence: | |||
# '''For add-type operations:''' Create a new blank row to accomodate the new entry. | |||
#* If the table uses an in-line form, make a request to the server for the default value, and push that value set into '''data_raw''' attribute for now. | |||
#* If the table employs a full-form, prepare the value to populate the {{code|id}} field of the form. This value is fed to the form via the function {{code|activate_tbform}} ''(Step 3)''. | |||
# Switch the table's button set from the standard '''"add-edit-delete"''' set to show the '''"OK-cancel"''' set instead. | |||
# Trigger the editable state of the form. | |||
# Activate the table's form. | |||
#* For a table with an in-line form, populate the form with the proper data set from '''data_raw'''. Then, switch out the cell texts with form fields hidden in the table's '''haven''' element. | |||
#* For a table with a full-form, fill the form's {{code|id}} field with proper value, trigger a change event on said field, then show the modal. Different values imply different operation as detailed below: | |||
#** '''A blank string''' implies a totally new entry. | |||
#** '''A positive integer''' implies an edit/delete operation on the entry with this ID. | |||
#** '''A negative integer''' implies a copy operation on the entry whose ID is the positive value of this integer. | |||
'''NOTE:''' A delete operation follows roughly the same procedure, however, with cell-content switching (for in-line forms) and full-form modal showing (for full-forms) skipped. The delete prompt is immediately shown right after the listed steps are completed. | |||
==== Form Deactivation ==== | |||
Once the add/edit/delete operation is submitted or cancelled, the sequence detailed below is triggered. | |||
# '''For successful operations:''' Commit the change to the CuneiTable. | |||
#* For add/edit operations: Replace the cell texts (currently residing in the '''haven''' element) with the final values as confirmed by the server via '''{{code|data.entry}}'''. ''(See [[CuneiForm#Non-redirecting Form|Submission of Non-redirecting Form]] for details.)'' | |||
#* For delete operations: Delete the coresponding row from the CuneiTable. | |||
# Switch the table's buttons back to show the standard '''"add-edit-delete"''' set. | |||
# '''For tables in link-mode:''' Reprocess and rebind the link destination of a row. | |||
# Untrigger the form back to its disabled state. | |||
# Clear all search modals related to the form to prevent search result retention. | |||
# '''For a table using an in-line form:''' Switch out the form fields with the cell texts hidden in the table's '''haven''' element. | |||
== Useful Server-side Patterns == | == Useful Server-side Patterns == | ||
=== Shortcut | === Simple Search Table === | ||
<syntaxhighlight lang='python' line=1> | |||
@blueprint_name.route("<submit_path>", methods=["POST"]) | |||
def function_name(table=None, prefix="ChqSchTable", *kwargs): | |||
if table is None: | |||
req_prefix = request.args.get("prefix", None) | |||
prefix = prefix if req_prefix is None else req_prefix | |||
table = TableClass(prefix=prefix, searchable=True, editable=False) | |||
identifier = json.loads(request.form.get("sel_iden" if request.form.get("mode")=="sel" \ | |||
else "identifier")) | |||
# Extract variables and parameters from identifier and/or request. | |||
# ... | |||
# ... | |||
if request.form.get("mode") == "sel": | |||
selected_rows = request.form.get("identifier") | |||
try: | |||
selected_rows = json.loads(selected_rows) | |||
except (TypeError, json.decoder.JSONDecodeError): | |||
return jsonify({"gr_result":"failure"}) | |||
# Retrieve all relevant entries from a database. | |||
selected_rows = ... | |||
if len(selected_rows) == 0: | |||
return jsonify({"gr_result":"failure"}) | |||
else: | |||
# Re-format the results as a list of dict. | |||
response_data = [ {col0:row0_value0, col1:row0_value1, ...}, | |||
{col0:row1_value0, col1:row1_value1, ...}, | |||
... ] | |||
return jsonify({"data":response_data, "gr_status":"success"}) | |||
if request.form.get("mode") == "qsch" and not(table.multi): | |||
# Try fetching exactly ONE matching result from the server. | |||
# ... | |||
# If the result count is exactly ONE, the quick-search is successful. | |||
if ...: | |||
# Reformat the result as a dict. | |||
return jsonify({"data":qsch_result, "qs_status":"success"}) | |||
# Now that the quick-search has failed, grab everything and return. | |||
# ... | |||
# Re-format the results as an array of array. | |||
# The order in each sub-array reflects the table's columns. | |||
response_data = [ [row0_value0, row0_value1, row0_value2, ...], | |||
[row1_value0, row1_value1, row1_value2, ...], | |||
... ] | |||
return jsonify({"data":response_data}) | |||
# NOTE THAT this is an extremely simple variation. In practical cases, | |||
# the function 'fetch_all' can retrieve records from the database while | |||
# simultaneously applying the quick-search filter. So, the quick-search | |||
# section might get switched around a bit. | |||
</syntaxhighlight> | |||
=== Search Table with a Stand-alone Page === | |||
This example is extracted from the route that handles the [[#Shortcut]] form below. So, it can seem excessive in places. Feel free to cut and trim when adapting this pattern to your need. | |||
<syntaxhighlight lang='python' line=1> | |||
@blueprint_name.route("path_name", methods=["GET", "POST"]) | |||
def function_name(ref_tbname=None): | |||
# Handles different references to the table, | |||
# in case some pages need 5 product search modals, for example. | |||
ref_tbname = tbname if ref_tbname is None else ref_tbname | |||
# Check the permission(s). | |||
table.perm_bit = ... | |||
if table.perm_bit == 0: | |||
abort(403) | |||
# FETCH implies the client is merely requesting the data, not the page. | |||
fetch = True if request.args.get("fetch") == "yes" else False | |||
if fetch: | |||
# 'NCOL' FETCH MODE: For new entry in a table with an in-line form. | |||
# This section can be simpler if one doesn't desire the COPY function. | |||
if request.form.get("mode") == "ncol": | |||
identifiers = json.loads(request.form.get("identifier")) | |||
copy_id = request.form.get("copy_id") | |||
# ... | |||
# ... | |||
# Return data in list form. Member order reflects the table's columns. | |||
return_data = [...] | |||
return jsonify({"data":return_data}) | |||
# 'GRAB' FETCH MODE: Retrieve a full entry to populate the table's full-form. | |||
elif request.form.get("mode") == "grab": | |||
identifiers = json.loads(request.form.get("identifier")) | |||
thisid = identifiers.get("id", None) | |||
# ... | |||
# ... | |||
# Return data in dict form. | |||
return {...} | |||
# 'SEL' FETCH MODE: Retrieve selection results. | |||
elif request.form.get("mode") == "sel": | |||
selected_rows = request.form.get("identifier") | |||
try: | |||
selected_rows = json.loads(selected_rows) | |||
except (TypeError, json.decoder.JSONDecodeError): | |||
return jsonify({"gr_result":"failure"}) | |||
# Retrieve all relevant entries from a database. | |||
selected_rows = ... | |||
if len(selected_rows) == 0: | |||
return jsonify({"gr_result":"failure"}) | |||
else: | |||
# Re-format the results as a list of dict. | |||
response_data = [ {col0:row0_value0, col1:row0_value1, ...}, | |||
{col0:row1_value0, col1:row1_value1, ...}, | |||
... ] | |||
return jsonify({"data":response_data, "gr_status":"success"}) | |||
# 'QSCH' FETCH MODE: Retrieve quick-search result OR the whole table. | |||
if request.form.get("mode") == "qsch" and not(table.multi): | |||
# Try fetching exactly ONE matching result from the server. | |||
# ... | |||
# If the result count is exactly ONE, the quick-search is successful. | |||
if ...: | |||
# Reformat the result as a dict. | |||
return jsonify({"data":qsch_result, "qs_status":"success"}) | |||
# Now that the quick-search has failed, grab everything and return. | |||
# ... | |||
# Re-format the results as an array of array. | |||
# The order in each sub-array reflects the table's columns. | |||
response_data = [ [row0_value0, row0_value1, row0_value2, ...], | |||
[row1_value0, row1_value1, row1_value2, ...], | |||
... ] | |||
return jsonify({"data":response_data}) | |||
# NOTE THAT this is an extremely simple variation. In practical cases, | |||
# the function 'fetch_all' can retrieve records from the database while | |||
# simultaneously applying the quick-search filter. So, the quick-search | |||
# section might get switched around a bit. | |||
# POST method (after the FETCH filter) implies form submission. | |||
if request.method == "POST": | |||
tbform = table.fullform[3] if table.fullform else table.form | |||
# When a database is involved, it is recommended to use 'gen_dbcommit_resp'. | |||
# Many things are taken care of inside. | |||
db_mod_code, new_entry, resp_dict = gen_dbcommit_resp(dbtype, tbname, model, | |||
tbform, table.perm_bit, | |||
suppress_success_msg=True) | |||
if resp_dict == 403: | |||
abort(403) | |||
# If this is not a delete operation, prepare and package resp_dict['entry'] too. | |||
# resp_dict['entry'] is a list of values. Its order reflects the table's columns. | |||
if isinstance(db_mod_code, int) and db_mod_code < 4 and not(new_entry is None): | |||
# ... | |||
resp_dict["entry"] = [...] | |||
return jsonify(resp_dict) | |||
# At this point, it is clear that the client is requesting the stand-alone page. | |||
table.populate_route = url_for("<blueprint>.<function_name>", fetch="yes") | |||
if table.fullform: | |||
table.fullform[3].populate_id = ["id"] | |||
table.fullform[3].populate_route = table.populate_route | |||
# The template 'browse_table.html' is suitable for most stand-alone table page. | |||
# Using it is convenient but not necessary. | |||
return render_template("browse_table.html", ...) | |||
</syntaxhighlight> | |||
==== Shortcut ==== | |||
<syntaxhighlight lang='python' line=1> | |||
@blueprint_name.route("path_name", methods=["GET", "POST"]) | |||
def function_name(mes_dict=None, prefix="DefaultPrefix"): | |||
this_permission = ... | |||
import_form = StdImportForm.gen_import_form("blueprint_name.function_name", upload="yes")) | |||
if request.args.get("upload", False) == "yes" and this_permission >= 5: | |||
# Do import sequence | |||
return ... | |||
if request.args.get("export", False) == "yes" and this_permission >= 1: | |||
# Do export sequence | |||
return ... | |||
real_mes = interpret_mes_request(request.args, 3) if mes_dict is None | |||
else interpret_mes_request(mes_dict, 3) | |||
req_prefix = request.args.get("prefix", None) | |||
prefix = prefix if req_prefix is None else req_prefix | |||
table = TableClass(prefix=prefix, suppress_copy=False, **real_mes) | |||
table.sch_tbs = [] | |||
# Append search tables into table.sch_tbs | |||
# ... | |||
dbtype, tbname = "table", "<database_file_name>" | |||
# If the table name and the database name do not match. Also do: | |||
# ref_tbname = "<table_name>" | |||
model, module = CuneiModelClass, "<module_code>" | |||
new_default = function_that_return_default_val_dict | |||
fetch_all = function_that_return_all_entries_in_the_database | |||
return single_tb_page(dbtype, tbname, model, module, table, new_default, fetch_all, request, | |||
import_form=import_form, | |||
export_route=url_for("blueprint_name.function_name", export="yes"), | |||
pin_args=..., | |||
back_route=..., | |||
ref_tbname=ref_tbname) # If necessary. | |||
</syntaxhighlight> | |||
== Future Development == | == Future Development == |
รุ่นแก้ไขปัจจุบันเมื่อ 15:06, 20 มิถุนายน 2567
CuneiTable is designed specifically to enable in-line data entry. Design-wise, it is more limited than CuneiForm since design flexibility is not highly expected from table-type objects. In essence, CuneiTable is a wrapper for the accompanying CuneiForm, whether that form be the in-line type or the full expansion type.
Define
Defining a CuneiTable is essentially declaring a CuneiForm with a few additional class variables. Below is an example of a very basic CuneiTable:
class AccCodeTable(CuneiTable):
MASTER_colsizes = [10, 20, 50, 20]
MASTER_colnames = ["StID", "Account Code", "Account Name", "Account Group"]
MASTER_coltypes = ["int", "str", "str", "sel"]
MASTER_choicecols = ["accgrp"]
MASTER_choices = [[
("1", "Asset"),
("2", "Liability"),
("3", "Equity"),
("4", "Revenue"),
("5", "Expense")
]]
MASTER_firstonly = ["stid"]
class SubForm(FlaskForm, CuneiSubForm):
stid = StringField("StID")
code = StringField("Code", validators=[InputRequired("Required!")])
name = StringField("Name", validators=[InputRequired("Required!")])
accgrp = SelectField("Group", default=1)
def validate_stid(self, stid):
# Validation goes here
The subclass SubForm is defined like the stripped down version of CuneiForm. Only put fields and validation logics here. All other behaviours are controlled by the MASTER variables from the CuneiTable level.
NOTE THAT neither of the Submit-delete Field Set (submit
, is_del
, and del_submit
) is declared in the SubForm. These fields are taken care of automatically during CuneiTable initiation.
The Holy Trinity of CuneiTable
The 3 MASTER variables listed below are required for all CuneiTable classes.
MASTER_colnames
([str,]): The list of column headers to appear on the page.MASTER_coltypes
([str,]): The list of column value types. Available types include:- All types listed in the main article for CuneiForm.
'str'
: Normal string value.'check'
: Value represented by a checkbox.'sel'
: Value with multiple choices. In other words, this is equivalent to a drop-list field in forms. In the same fashion, the displayed values for this type is not exactly the same as the values stored in the database. The raw value must pass through a map first before being displayed.'rad'
: Similar to'sel'
, but this value type is represented by radio buttons in multiple sub-columns instead.'submit'
,'del_submit'
: Submit button. (Not handled manually, they are handled automatically during initiation.)
MASTER_colsizes
([float/int,]): The list of relative column sizes. CuneiFox will calculate the display sizes automatically.
NOTE #1: The member order in each of these lists follow the definition order of fields in the SubForm. DO CHECK that all of these 3 lists and the SubForm fields share the same length.
NOTE #2: On the client-side, a radio field ('rad'
) is displayed as multiple sub-columns (one for each choice). Hence:
- The display headers of sub-columns are not dictated by the corresponding value in MASTER_colnames, but by the values in MASTER_radios (Detailed in a sub-section below) instead.
- The corresponding member in the MASTER_colsizes list is NOT merely one numerical value, BUT a list of numerical values instead.
Choices and Radios
Columns with limited choices of predesignated values should utilize 'choice' or 'radio' representation; especially when the stored values must be mapped to another set of more readable or localizable display values. Ideally:
- Columns utilizing choice representation is typed
'sel'
(MASTER_coltypes[i] = 'sel'
), and the corresponding field in the SubForm is a SelectField. - Columns utilizing radio representation is typed
'rad'
(MASTER_coltypes[i] = 'rad'
), and the corresponding field in the SubForm is a RadioField.
MASTER_choicecols = [str choice_col0, str choice_col1, ...]
MASTER_choices = [
[ (str col0_choice0, str col0_display0),
(str col0_choice1, str col0_display1),
... ],
[ (str col1_choice0, str col1_display0),
(str col1_choice1, str col1_display1),
... ],
...
]
MASTER_radiocols = [str choice_col0, str choice_col1, ...]
MASTER_radios = [
[ (str col0_choice0, str col0_display0),
(str col0_choice1, str col0_display1),
... ],
[ (str col1_choice0, str col1_display0),
(str col1_choice1, str col1_display1),
... ],
...
]
An example can be seen in the code at the top of the article.
NOTE #1: Take care that MASTER_choicecols and MASTER_choices have the same length. Similarly for MASTER_radiocols and MASTER_radios.
NOTE #2: The SelectField and RadioField defined in the SubForm DOES NOT include the choices. (Again, see example at the top.)
Search and Fill
Since the in-line form of a table is programmatically generated, the search-fill function cannot wait until the designing step like CuneiForm's does. The search-fill function of an in-line form is defined in this step via the variable MASTER_schfill.
MASTER_schfill = {
str search_col0: [str search_spec0, str fill_spec0],
str search_col1: [str search_spec1, str fill_spec1],
...
}
# Example from Bank Account table
MASTER_schfill = {
"bank_code": ["ModalBank:code",
"bank_stid:stid,bank_name:name,bank_branch:branch"],
"acccode_code": ["ModalAccCode:code",
"acccode_name:name,acccode_stid:stid"]
}
NOTE: The formats of search_specX and fill_specX is similar to arguments search and fill in CuneiForm's render_blank macro.
Instant Calculation
Instant calculation function within in-line forms, or 'instacalc', allows a change in one form field to initiate a small calculation request to the server and update other affected fields based on the calculation result. It is specified in the variable MASTER_instacalc
of the CuneiTable.
(Refer to the dedicated sub-section in CuneiForm article for format and example.)
Auto-numbering Function
In-line forms with auto-numbering specifications work with CuneiFox core system to serve auto running numbers in a particular format (usually for document numbers). The specifications for this feature are embedded within MASTER_autorun
of the CuneiTable.
(Refer to the dedicated sub-section in CuneiForm article for format and example.)
Table with a Separate Form Modal
Sometimes, the entries have either too many columns or columns not suitable for display in the table format. In such cases, developers can use the full-form option. CuneiTable with an associated full-form is only defined with essential columns for a 'quick glance' view. The full form is shown in a pop-up modal for entry addition/edit.
A CuneiTable can be linked to a full-form via the class variable MASTER_fullform which takes the following format:
MASTER_fullform = [str module_code, str fullform_class_name,
str fullform_modal_header_text, str modal_size]
# Example from Product table
MASTER_fullform = ['cunei_iv', 'ProductFullForm', 'Product Information', 'xl']
- size dictates the modal size displayed on the client-side. It utilizes BootStrap 4's
'modal-<size>'
classes. Available choices are'sm'
,'md'
(corresponds to default modal size),'lg'
, and'xl'
.
NOTES on full-form class and macro:
- The CuneiForm class
fullform_class_name
must be found undermodule_code
in the filecuneifox/<module_code>/forms.py
orcuneifox/<module_code>/forms.pyc
. - The definition of the full-form class DOES NOT include the Submit-delete Field Set
- The Jinja2 macro for the full-form design must also be named
fullform_class_name
and must be found undermodule_code
in the filecuneifox/<module_code>/template/<module_code>/form_macro.html
. - Refer to the main CuneiForm article for more details on form definition and design. For information specific the full-form macros, refer to a dedicated sub-section below.
In-line Form with Expansion
Some tables contain just a tab too many columns, but not enough to warrant a fully-fledged separate form. In this case, CuneiTable allows its in-line form to have some fields drawn separately in an expansion modal. This behaviour is controlled by the variables MASTER_expand, MASTER_tbexpand, and MASTER_manualdraw.
MASTER_expand = {str trigger_field0: str expand_spec0,
str trigger_field1: str expand_spec1,
...}
MASTER_tbexpand = str expand_spec
MASTER_manualdraw = [str expand_field0, str expand_field1, ...]
# Example from the product table on the 'Product Delivery' page
MASTER_expand = {"product_code_mock":"BodyEx1:product_name"}
MASTER_tbexpand = "BodyEx1:product_name"
MASTER_manualdraw = ["product_code", "product_name", "desc", "manage_serial", "serial_nos"]
- trigger_field: The field with an Expand button
'<expand_modal_id>:<first_expand_field>'
attached when the form is triggered. - expand_spec: The modal and field to focus on once the Expand button is presses. (Shares the format with the argument expand in CuneiForm's render_blank macro.)
- expand_field: The field to be skipped during dynamic in-line form generation. Fields listed in MASTER_manualdraw must be put on the page manually.
Injections & Post-submission Logics
Some columns, while not stored directly in the database, are quite essential, or at least useful, to display on the client-side table. In CuneiFox's normal operation, whether it be a data fetch request or a display table update after a database commission, the server returns (representation of) entries from within the database. This ensures the client-side is shown the most updated data, but it also means that columns not in the database do not show up by default. Variables MASTER_preserve_across and MASTER_inject_func seek to better this behaviour.
Employing a similar routine, variable MASTER_post_submit_func allows additional logic to be run after a database commit from a CuneiTable.
MASTER_preserve_across
: (This feature is active AFTER SUBMISSION) A list of field names, present in the in-line form but not in the database, whose values are read from the form and reinjected into the data sent to client for table manipulation.MASTER_inject_func
: (This feature is active ON SINGLE-ENTRY FETCH) A list of functions (and their arguments) to run just before the fetched entry is sent to the client-side.MASTER_post_submit_func
: (This feature is active AFTER SUBMISSION) A list of functions (and their arguments) to run just after a database commit. This feature is useful both for additional logic run and to modify the returned entry (in the same vein as inject_func does for a fetch request).
MASTER_preserve_across = [str field_name0, str field_name1, ...]
MASTER_inject_func = {'func': <function_object>,
'args': <list_of_ordered_arguments>}
MASTER_post_submit_func = {'func': <function_object>,
'args': <list_of_ordered_arguments>}
# Example from (former) tax ID table
MASTER_preserve_across = ["bcount"]
# Example from product group table
MASTER_inject_func = {"func":defvat_inject, "args":[]}
# Example from account book table
MASTER_post_submit_func = {"func":accbook_to_docseries, "args":[]}
NOTE #1: preserve_across feature is automatically assumed for columns covered by CuneiModel's Cross-table references. There is no need to list such columns in MASTER_preserve_across.
NOTE #2: At the time of writing, CuneiTable's injections & post-submission logics are handled automatically only on the #Shortcut Route for Single-table Page & Search Table.
Other Table Definition Variables
Other notable MASTER variables that can be set upon form definition include:
MASTER_firstonly
: A list of field names that are only active (editable) for a new entry, but inactive (not editable) when editing an old entry.MASTER_pseudohides
: A list of field names for which the corresponding fields are not rendered on form triggering.MASTER_delconfirm
: The specification for the delete confirmation modal. This value shares the format with the argument confirm_first in CuneiForm's render_blank macro. The default value is['This will delete...', 'code']
MASTER_firstonly = [str field_name0, str field_name1, ...]
MASTER_pseudohides = [str field_name0, str field_name1, ...]
MASTER_delconfirm = [str confirm_modal_header, str field_name_to_confirm]
# Example from account code table
MASTER_firstonly = ["stid"]
# Example from journal setting table
MASTER_pseudohides = ["book", "colspec"]
# Example from Accounting Journal's VAT table
MASTER_delconfirm = ["This will delete...", "taxdoc"]
Initiate
CuneiTable class has its own __init__
function.
__init__(self, prefix="", **kwargs)
Parameters |
|
The rough step-by-step of a CuneiTable's initiation is as follows:
- Assign the following MASTER variables to corresponding attributes of the CuneiTable instance (if the attributes are not specified otherwise):
- Modify the full-form class with standard field set, initiate the full-form, and modify the form instance's attributes (where applicable).
- Modify the in-line SubForm class with standard field set, initiate the in-line form, and modify the form instance's attributes.
- Assign the choice and radio MASTER variables to corresponding attributes of the CuneiTable instance (if the attributes are not specified otherwise). Then, use the attributes to modify the choices of the in-line form fields.
- Add values corresponding to the standard column set to the 3 main MASTERs.
Standard Column Set
CuneiTable requires a few standard columns in order to function properly. These columns need not be defined explicitly, but are programmatically generated and integrated into all CuneiTable instances. The 4 standard columns include:
- id, submit, is_del: These columns correspond to the #Submit-delete Field Set of the corresponding in-line form or full-form. Developers can also choose to explicitly define these columns in the class definition (especially the id column) if they desire.
- The multi-selection column: This column is only created for a table with multi-selection enabled. It is displayed as the leftmost column of the table. (See the note on multi attribute below.)
Column Sizes & Post-init Modification
For easier development, column sizes are defined (via MASTER_colsizes) in relative sizes. However, those values are not used directly in the client-side render. They would have to converted to percentage values first via an internal function. While developers rarely need to tamper with column sizes after class definition, there are cases where tables are reused for many pages and with some columns hidden (in other words, resized to 0) in some of its incarnation.
Column resizing, post-initiation, is done by running 2 function consecutively: assign_csize
and tune_cols
.
assign_csize(self, cname, csize)
Parameters |
|
Notes | At this stage, the CuneiTable instance have alrady gone through one round of size tuning. Hence, the value csize should be set to the new relative (100-based) basis. |
tune_cols(self, skip_multicheck=False)
Parameters |
|
Notes on Keyword Arguments
The following sub-sections introduce notable keyword arguments. Other keywords not detailed here are also welcome during CuneiTable initiation. Unless explicitly specified, all keyword arguments can be accessed post-initiation as attributes of the created CuneiTable instance.
MASTER-adjacents
Back to the #Define section, we are introduced to a number of MASTER variables. Those variables set at table definition act as the default values for each table type. However, developers can feed an overriding value that applies only to the CuneiTable instance being initiated via a corresponding non-master keyword (e.g. use keyword instacalc to override the class's MASTER_instacalc).
The option to modify object-specific values after initiation is also available. However, since value packing and manipulation occurs during initiation, some details should be noted:
- colsizes, colnames, expand, tbexpand, manualdraw, del_confirm (corresponds to MASTER_delconfirm), preserve_across, post_submit_func, inject_func, and pseudohides are normal table attributes and directly take on the values of their MASTER counterparts. They can easily be accessed and modified.
- radiocols and choicecols take on the values of their MASTER counterparts. These 2 attributes work closely with the next item on this list.
- radios and choices take on the values of their MASTER counterparts. These attributes are used to generate a translation map on the client-side to aid value display. CuneiTable's initiation routine also includes assigning these values as the choices attributes of corresponding in-line form (including expansion) fields as well.
- schfill takes on the value of MASTER_schfill. This attribute applies only to in-line form (dynamically-generated) fields. Fields under manualdraw must have equivalent values specified during the form designing phase.
The following attributes do not allow value feeding during initiation phase. However, they can be modified after the fact if needed, so it is fit to discuss them here. (SubForm and FullForm referenced here can be accessed via appropriate CuneiTable attributes. See #Form-related Attributes sub-section.)
- SubForm.firstonly and FullForm.firstonly: For an in-line form, the form attribute is copied directly from MASTER_firstonly. For a full-form, members from MASTER_firstonly are appended to what the form originally has by itself.
- SubForm.autorun and FullForm.autorun: For an in-line form, the form attribute is copied directly from MASTER_autorun. For a full-form, members from MASTER_autorun are appended to what the form originally has by itself.
- SubForm.instacalc takes the value of MASTER_instacalc. Its post-init format can be found in the CuneiForm article. Note that MASTER_instacalc only affect in-line form fields.
- SubForm.coltypes is taken (and modified to accommodate the standard column set) from MASTER_coltypes. The initation routine also includes extracting appropriate values from this list to set the special_type field attributes for in-line form as well.
Know Thy Self
These attributes dictate the way a CuneiTable is drawn on the client-side along with ways to identify its affiliated components:
- _id (str): The initiation argument 'prefix' repacked. This value will become the HTML id of the table object. (The attribute name is underscored to avoid conflict.)
- in_modal (str: defaults to
False
for stand-alone tables): The ID of the modal with this table as the MAIN SUBMITTING COMPONENT, like the search modal affiliated with a search table, for example. (See CuneiModal for details.) - bound_modal (str: defaults to
False
): The ID of the modal in which this table is drawn but IS NOT the MAIN SUBMITTING COMPONENT. - slim (bool: defaults to
False
): Whether to render the in-line form in slim mode. When supplied as an init argument, it is transfered directly to SubForm.subslim. (For CuneiTable's in-line forms, the difference is less obvious than in full-blown forms. Here, slim mode only saves vertical spaces with reduced padding.) - linkmode (list: defaults to
False
): If set, the CuneiTable is not for selection. Each row of the table acts as a navigation link to another page. This attribute take the format['<url_for_route_str>', ['<argument_col0>', '<argument_col1>', ...]]
- multi (bool: defaults to
False
): Whether the table is able to make multiple selections at a time. If that is the case, a special column is automatically set up for this feature (see #Standard Column Set). - searchable (bool: defaults to
True
): Whether to draw a live-search field at the top of the table (see #Live Search).
Editable or Read-only
The attribute editable (bool: defaults to False
) dictates whether a CuneiTable is edit-able at all. However, the finer details of which actions are allowed or suppressed are more finely tuned via the following attributes:
- perm_bit (int: defaults to
1
): The permission bit dictating what actions the useris allowed to do. It mimics the integer permission bits in the main CuneiFox system, namely:- 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. (Even if the editable attribute is set to
True
, the user will still find all non-read buttons dimmed out.) - 2: Add. (Allows 'add' type actions: add and copy.)
- 3: Add + Edit.
- ≥4: Add + Edit + Delete.
- suppress_add (bool: defaults to
False
): Whether to suppress the drawing of the table's Add button . (Suitalbe for cases where entries are supposed to be edit-only.) - suppress_copy (bool: defaults to
True
): Whether to suppress the drawing of the table's Copy button . - suppress_del (bool: defaults to
False
): Whether to suppress the drawing of the table's Delete button .
Sequence and Columns
- seq and cnames attributes of a CuneiTable are copied to its in-line form. These attributes do not support value feeding during initiation because they are highly involved in the process. In fact, it is NOT RECOMMENDED to trifle with them at all. Their functions can be found in the corresponding section on the CuneiForm page.
- content_width (str): The width of the table itself (not the bounding element of the table) on the client-side display. The value can be anything interpretable as CSS size.
Before we go on and make references to the forms themselves, let us take a little detour to see how to control the appearances of table-bound forms on the client-side.
- An in-line form is not much of a hassle, and user only loses the ability to manipulate entries (the table becomes read-only), but not any information, when it is not drawn. So only the editable discussed above is all we need. The form is only drawn only when
table.editable = True
. - A full-form is more complicated. For example, it might need to be drawn even if the table is read-only because losing it at all means losing the ability to see the full entries'. By default, a full-form is always drawn, the editable attribute only controls the read-only status of the form.
If the full-form is not wanted at all (for example, when a full-form entails too many search pop-ups that threaten to clutter the user experience), developers have to choices to:- Feed the keyword argument
suppress_fullform=True
at CuneiTable initiation. - Use keyword argument
skip_fullform=true
on BOTH the general_table_block and general_table_tail macros in an HTML template.
- Feed the keyword argument
Another notable attribute is modal_view (int: 0, 1, or 2). This attribute applies to a full-form. It acts as the flag to control the drawing of the full-form itself and the associated Edit , View and Delete buttons. Its value is automatically interpreted during initiation and requires NO MANUAL MANIPULATION. Nevertheless, its value might be useful to developers:
0
: The table has no full-form defined or the it is suppressed at initiation.1
: The table has an editable full-form and relevant buttons will be drawn. Whether the buttons are usable is determined by the table's perm_bit attribute.2
: The table has a read-only full-form and only the View button is drawn.
With form-control discussed, we are now ready. The following 2 attributes refer to the forms bound to the CuneiTable. These attributes DO NOT support value feeding during initiation.
- form refers to the table's in-line form.
- fullform is a list of values related to the table's full-form. When applicable, the attribute reads and repackages MASTER_fullform into the format below:
table.fullform = [str fullform_modal_header,
str modal_class, # OBSOLETE
Jinja2_Macro form_design_template,
CuneiForm fullform_instance,
str modal_size]
# An example for full-form attribute manipulation post-initiation.
product_tb.fullform[3].slim = True
NOTE #1: The FullForm instance itself has a notable attribute modal_addi_btns that dictates additional buttons in the full-form modal (besides the default OK and Cancel buttons). The value is only modifiable post-initiation, as in the example below:
table.fullform[3].modal_addi_btns = \
[ [str btn_name0, str btn_label0, str_or_list btn_class0, str function_name0], ... ]
# Example from withholding tax invoice table in the accounting journal page.
wht_tb.fullform[3].modal_addi_btns = [["prn", "Print", "dark", "whtprn"]]
- btn_nameX is used to id the button element on the client-side as
'<table._id>_FullFormModal-<btn_name>btn'
. - btn_labelX is the label appearing on the button.
- btn_classX is the color scheme of the button. It can take the format of either the
class
orcustom_color
arguments of CuneiForm's render_btn macro. - function_nameX is the name of the JavaScript function to be called when the button is pressed.
NOTE #2: The obsolete modal_class member in the fullform attribute is a relic of the past when the table and modal headers are limited to preset colours in the BootStrap template. In the current version, the colours can be more freely set, but the full-form modal must shares the same color scheme with the parent table to emphasize their relationship.
Getting Data
Under the normal work flow, CuneiFox sent only blank components to the client first to render the page. At this stage, a CuneiTable only displays as a header with a blank body. Once the page components are loaded, the elements (including CuneiTables) can then fetch data from the server, each using a separate HTML request.
The attributes populate_route, populate_suppress, and populate_id dictate the table's behaviours regarding data request. See explanation on them in CuneiForm#Getting Data.
NOTE #1: CuneiTable supports multiple fetch modes listed below:
'qsch'
is for populating the entire table. If the table is a part of a search-fill routine of another form, the table is populated only after the quick-search fails to fetch a unique selection.'sel'
is used to fetch the full database entries for the current selection(s). This mode is triggered when the selection(s) of a search modal is properly committed.'free_grab'
fetches one active entries to populate a table's full-form. This mode is equivalent to the fetch mode'grab'
on stand-alone AND in-line forms. Details on'grab'
mode can be found under NOTES in CuneiForm#Getting Data.
NOTE #2: For a CuneiTable with a full-form, the form's populate_id is automatically assumed to be similar to the table's (with change trigger if the fetch mode 'free_grab'
is not suppressed) PLUS the column name 'id'
.
Sending Data
Both in-line form and full-form submit in a non-redirecting fashion by CuneiFox default. So, the only important attribute regarding form submission via a CuneiTable is:
- post_route (url_for_string: defaults to
False
): Route to send a submit request.
In rare cases where redirection is needed, developers can either modify the appropriate attributes of the in-line form and/or full-form (see CuneiForm#Sending Data), OR package a redirect or reload command with the submission response (see CuneiForm#Non-redirecting Form).
Design & Pre-made Scripts
Now that a CuneiTable is defined, initiated, and sent along with a template, here comes to frontend part. CuneiFox's standard pattern for putting a table up on an HTML page includes:
- Design a full-form macro. (Only when applicable. Done once and reuse for all repeated instances.)
- Initiate a
<table>
DOM element and a Javascript table object. Both fall under the scope of the Jinja2 macrogeneral_table_block
. - Design the pop-up modal for the extension (manually drawn) parts of the in-line form. (Only when applicable. See CuneiModal#Design & Pre-made Scripts for details.)
- Finalize the Javascript object and bind relevant events with the Jinja2 macro
genetal_table_tail
.
One can see that, for the simplest tables, only two lines of macros is sufficient.
/* Full-form macro lives elsewhere and not shown here. */
{{ CuneiTables.general_table_block(table, block_height, ...) }}
/* If in-line form has an extension, the extension modal goes here */
{{ CuneiModals.modal_upper_half('<ext_modal_name>', ...) }}
...
...
{{ CuneiModals.modal_lower_half('ext_modal_name', ...) }}
/* End of form extension block */
{{ CuneiTables.general_table_tail(table, ...) }}
Full-form Macro
The macro of a CuneiTables' Full Form is to be written in the file cuneifox/<module_code>/templates/<module_code>/form_macro.html
directory. In essence, each full-form macro is merely a CuneiForm design with a few notable properties.
- The Jinja2 macro for the full-form must be named after the
fullform_class_name
specified in the corresponding CuneiTable class attribute. (See #Table with a Separate Form Modal for details.) - To avoid name conflicts of the form's serach modals with other modals on the page, a full-form macro is recommended to accept and work with
modal_shift
argument. - To avoid having to create a full-form system for every small variation in use cases, a full-form macro is recommended to accept and work with
schcol_shift
andfillcol_shift
arguments.
Note that requirements 2 and 3 are not at all compulsory and can totally be ignored if there is no risk of modal name conflicts or usage variation. However, due to these requirements, the pattern of a typical full-form macro is as shown below.
{% macro <macro_name>(form, draw_submit=true, hide_submit=false,
confirm_first=[gettext("This will delete..."), "code"],
modal_shift={}, schcol_shift={}, fillcol_shift={}) %}
{% set sch_modals = {"<key0>": "<default_search_modal_name0>", ...} %}
{% set sch_cols = {"<key0>": "<default_search_column0>", ...} %}
{% set fill_cols = {"<key0>": "<default_fill_specs0>", ...} %}
{% do sch_modals.update(modal_shift) %}
{% do sch_cols.update(schcol_shift) %}
{% do fill_cols.update(fillcol_shift) %}
{{ CuneiForms.general_form_head(form) }}
// Put form fields here.
{% if draw_submit %}
{% if not(hide_submit) %}
<div class="row align-items-start">
<div class="col">{{ CuneiForms.render_submit(form, form.submit) }}</div>
{% if form.del_submit %}
<div class="col">
{{ CuneiForms.render_submit(form, form.del_submit, class="danger",
confirm_first=confirm_first) }}
</div>
{% endif %}
{% if form.is_del %}
<div class="col">{{ CuneiForms.render_check(form, form.is_del) }}</div>
{% endif %}
</div>
{% else %}
{{ CuneiForms.render_submit(form, form.submit, hidden=true) }}
{% if form.del_submit %}
{{ CuneiForms.render_submit(form, form.del_submit, class="danger",
confirm_first=confirm_first, hidden=true) }}
{% endif %}
{% if form.is_del %}
{{ CuneiForms.render_check(form, form.is_del, hidden=true) }}
{% endif %}
{% endif %}
{% endif %}
<script type="text/javascript">
// Scripts are here.
</script>
{{ CuneiForms.general_form_tail(form, populate=false) }}
{% endmacro %}
In this pattern, specifications of search-and-fill fields can be a little clunkier than usual. For example, where one might normally write for a free-standing form...
{{ CuneiForms.render_blank(form, form.x_code,
search="ModalAccCode:code",
fill="stid:x_stid,name:x_name") }}
becomes...
{{ CuneiForms.render_blank(form, form.x_code,
search=sch_modals["xcode"]+":"+sch_cols["xcode"],
fill=fill_cols["xcode"]) }}
General Table Block & Table Object
general_table_block(table, block_height,
block_width=null, content_width=false,
is_master=false, pad_bottom=true,
head_class="dark", head_bg=false,
force_head_textwhite=false,
modal_shift={}, schcol_shift={}, fillcol_shift={},
skip_fullform=false, intab=false)
Parameters | DO NOT rely on argument order farther than table, block_height, and block_width, ALWAYS PROVIDE argument keywords.
|
The Working of General Table Block
In essence, the general_table_block
macro is responsible for the following actions:
- Create translation maps for choices and radios.
- Draw the full-form and its pop-up modal (if applicable).
- Register the table to the
all_tbfm_ids
list for future reference. - Create a
table_obj
corresponding to the table, and assign preliminary values to its many attributes. - Set up handler functions for the table action add, edit, delete, copy, submit, cancel, view, select, and follow link. (Each function is only set up once and shared among all the tables on the page.)
- Point the main component object attribute of the modal object referenced by table.in_modal (
modal_obj.main_comp_obj
) toward the newly createdtable_obj
(if applicable). - Draw the table's in-line form. (Only the General Form Head and the fields are handled at this stage.)
- Draw the table's live search field (if applicable).
- Draw the table's button set (if applicable).
- Draw blank table header and body elements.
- Programmatically fill in the table header.
Table Object Attributes
Below is the list of and notes on table object attributes:
- Basic Attributes
- id: Takes on the value of
table._id
. - self_type: Takes on value
'table'
.
- id: Takes on the value of
- Copied Attributes (See #Notes on Keyword Arguments for details.)
- cnames, colnames, colsizes, coltypes, editable, linkmode, modal_view, multi, perm_bit, populate_route, populate_suppress, pseudohides, searchable, seq, slim, and tbexpand: Takes on the exact value.
- csssizes: A list of reinterpretations of
table.colsizes
into the format ready to plug into CSS stylings. The sizes at this stage also takes into account the table block width, the table element width, and minimum column sizes. - populate_id: Takes on the exact value, with all prefix
#
cut once event-binding is taken care of. - in_modal and bound_modal: Point to the corresponding modal objects where applicable.
- intab: The id of the tab in which the
<table>
element belongs. - form: The id of the table's in-line form (
table.form._id
). - fullform: The id of the table's full-form (
table.fullform[3]._id
) when applicable. - choices: The value of
table.choices
with choices repacked as dict-type objects. - radios: The value of
table.radios
with choices repacked as Map objects.
- References to DOM Elements & Objects
- table_ele: Points to the corresponding
<table>
DOM element. - table_head_ele: Points to the corresponding
<thead>
DOM element. - table_body_ele: Points to the corresponding
<tbody>
DOM element. - progress_ele: Points to the corresponding progress bar
<div>
element. This might have already been a relic of the past, but it is still around in case it is needed. - searchbox_ele: Where applicable, points to the corresponding live-search
<input>
DOM element. - haven: Points to the corresponding haven
<div>
DOM element. The haven is where the row values live when they are switched out for in-line form fields, and where the form fields live when they are not active. - btngrp_eles: An array of button-related elements for easy references. Its members are, in order:
- Add
<button>
- Copy
<button>
- Edit
<button>
- View
<button>
- Expand
<button>
- Delete
<button>
- OK
<button>
- Cancel
<button>
- First Button Set
<div>
which is the parent of member 1 through 8 - Second Button Set
<div>
which is the parent of member 9 through 10
- Add
- row_eles: An array of
<tr>
elements for easy references. - check_eles: An array of row-selection checkbox
<input>
elements for each row. This attribute is only utilized in a table with multi-selection enabled. - link_eles: An array of row link
<a>
elements. This attribute is only utilized in a table whose linkmode is set. - mirror_ids: An array of element ids whose x-position must be synced with this table. Use cases include the element holding the summation fields.
- table_ele: Points to the corresponding
- Status Attributes
- progress_data: Human-readable loading status for the table. Localized strings for this element is stored in the variable
tbloadstr
. - progress_num: The number of currently loaded rows.
- populating: Whether the table in undergoing a population cycle. This state attribute prevents other populating-based routines to run in conflict.
- press_down_from_head: Whether to dispatch an ArrowDown event to table head after the table finishes populating. This creates the select-first-item behaviour.
- addingrec: Whether the current edit operation is of add-type (add or copy).
- last_copy_id: The id of the latest copied entry. This value is used to re-copy a new entry once the current operation is done.
- current_edit_idx: The row index of the currently editing/adding entry.
- triggered: Whether the table is the triggered state. (Only triggered tables can have their forms triggered as well.)
- unlockform: Whether the table's form is triggered.
- prevent_rowesc: Whether the Escape key on the row is prevented. This flag prevents Escape event from propagating up from the in-line form fields to the table row itself. This attribute exists because
event.stopPropagation()
fails to achieve the same effect in Firefox. - total_row: Current row count of the table.
- max_rowidx: Max integer used in the naming of row elements. One might think that this number should be the same as total_row, but that assumption is wrong as soon as some rows disappear upon deletion. (The value is
-1
for a blank table.) - counter_timeout: Timeout operation for hiding tb row counter. (The counter element slides into view when the mouse cursor is within the table's bound.)
- search_timeout: Timeout operation for the search operation via the search box. (The search is conducted 700ms after the search box value is last edited. This delay is enough to give the illusion of instancy while avoiding a lot of unnecessary operations.)
- Button Visibility Flags: Boolean flags dictating whether certain table buttons are rendered on the client-side. These attributes are interpreted from CuneiTable's own suppress_add, suppress_copy, modal_view, and perm_bit. There are 6 in total, namely addbtn_show, copybtn_show, editbtn_show, viewbtn_show, expbtn_show, delbtn_show.
- progress_data: Human-readable loading status for the table. Localized strings for this element is stored in the variable
- Event Handlers
- btngrp_handlers: The array of button click handler functions. Its members are, in order, the handlers for add, copy, edit, view, delete, ok, and cancel. Because these handlers either work with the table's selection routine or does not linked to any specific row at all, only one of each is needed.
- check_handlers: The array of select event handler for each table row.
- link_handlers: The array of link handler for each table row.
- Table Data
- data_raw: The raw data to be displayed in the table stored as an array or arrays. The values are those responded from the server and might not be ready in terms of human-readability, but they are great to work with programmatically.
- data: The data_raw in the displayed form.
- headrow_count: The number of row the table head has. The attribute defaults to
1
, but must be changed manually should any change is needed.
- Additional Functions & Routines
- post_load_cmds ([function,] or
false
): An array of functions to run at the end of a table-populating cycle. - post_submit_cmds ([function,] or
false
): An array of functions to run at the end of a submission cycle.
- post_load_cmds ([function,] or
- Selection-related Attributes
- last_select (
[int, boolean]
: defaults to[-1, false]
): An array of 2 members: the index of last select/deselect entries and its select/deselect state. This attribute is used to create the table's batch select/deselect behaviours (via ⇧ Shift+Click). - selections: The array of selected row indices.
- selected_entries: Raw data of the tables selected entries as sent from the server (in array format). Data stored here is used, for example, by search modals to mediate the data to the requesting form.
- selection_trackers: The array of tracker forn field specifications. In its final form, each of its members is a 2-array: the first sub-member is the form field element itself, the second sub-member is the name of the tracked column (as appears in cnames attribute). (See String & Text Area Fields for details on the form's side.)
- last_select (
Form Expansion Modal
This section only applies to CuneiTables with an expansion modal defined (see #In-line Form with Expansion).
In short, this section includes the design of a CuneiModal with form fields inside. Note that the form head and form tail sections are handled by general_table_block
and general_table_tail
respectively and need not be repeated here.
General Table Tail
At this point, the table_obj
and its corresponding form_obj
have been initiated, all fields and relevant elements are created; this final macro is to finalize the table's attributes and link all the elements together into a coherent working system.
general_form_tail(table, load_immediately=true, skip_fullform=false)
Parameters |
|
The Working of General Table Tail
The workings of this macro can be thus summarized:
- Run the general_form_tail for the table's in-line form or full-form.
- Process the table's populate_id attribute
- Interpret and repack each string value into a more easily usable format.
- Where applicable, bind a change event to the referenced fields so that they trigger a form re-populating routine when changes are detected.
- Finalize References to DOM Elements & Objects attribute group and bind related events.
- Bind events to show row counter element and outer glow to emphasize table's focus stage.
- Add button icons to the table's button sets.
- Bind keyboard shortcuts for the table's buttons.
- If the table is a search table, link the table's data load/unload behaviour to the corresponding search modal.
- Otherwise, link the table's data loading to the page's
load
event, unless prohibited via the argumentload_immediately
.
Mirror Table & Other Table Manipulations
Tables prove to be one of the simplest way to arrange elements of the interface. This section deals with table-related macros and functions that look at a table as a page formatting tool.
Mirror Table
In cases where free standing elements need to be aligned with some columns of an existing CuneiTable, some of the easiest options starts with drawing another table with the same column sizing as the original one, or mirror tables in CuneiFox's term. A mirror table drawn using this macro:
- Has the same number of column as the referenced CuneiTable.
- Has each column's size synced with the referenced CuneiTable.
- Has the horizontal scroll position synced with the referenced CuneiTable.
- Has the ID
'<id_of_ref_CuneiTable>_mirror'
. - Always has 1 blank header row to control the column sizing.
To draw a mirror table, use the Jinja2 macro draw_mirror_table
.
draw_mirror_table(table, block_width, content_width=false,
head_class="dark", head_bg=false, force_head_textwhite=false,
pad_bottom=false, slim=false, padx="yes", pady="yes",
row=1, head_row=0, border=false)
Parameters | DO NOT rely on argument order beyond table.
|
Blank Table
In more general cases, the table used in page formatting does not need to resemble an existing CuneiTable in any way. In such cases, a simple table can be drawn using the Jinja2 macro blank_table
.
blank_table(table_name, block_width="100%",
row=1, col=12, padx="yes", pady="yes",
cell_height=false, border=false)
Parameters | DO NOT rely on argument order beyond table_name.
|
Inserting Pre-drawn Elements into a Table
Now that we have our blank table, we can move pre-drawn elements into it. We will start with a more specific use case of insert form fields (and their corresponding labels) into a blank table via the JavaScript function stuff_form_to_tb
.
stuff_form_to_tb(form_name, table_name, stuff_arrays, is_mirror="auto")
Parameters |
|
// Example from Accounting Journal page (2 sum fields under the journal table.)
...
<div class="row justify-content-center pt-2">
{{ CuneiTables.general_table_block(vouch_tb, block_width="84vw", ...) }}
{{ CuneiTables.general_table_tail(vouch_tb, false) }}
<div class="w-100"></div>
<div class="d-none">
// First, draw form fields in a hidden block.
{{ 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) }}
</div>
{{ CuneiTables.draw_mirror_table(vouch_tb, block_width="84vw", ...) }}
<script type="text/javascript">
stuff_form_to_tb("{{ vouchsum_form._id }}", "{{ vouch_tb._id }}_mirror",
[["drsum", 0, 2], ["crsum", 0, 3]]);
</script>
</div>
...
While stuffing form fields and their labels into a blank table might constitute the majority of use cases, there are instances where other types of elements need to be inserted into a table. For these more general cases, we can utilize the Javascript function stuff_stuff_to_tb
.
stuff_stuff_to_tb(stuff_ele, tb_ele, rc_array, innerOnly=false, is_mirror="auto")
Parameters |
|
Cell Merging
To facilitate cell merging and to preserve CuneiFox style column sizing of the whole table, it is recommended that developers use the JavaScript function merge_cell
to perform cell merging, especially in CuneiTable-type tables and mirror tables.
merge_cell(table_name, merge_arrays, is_mirror="auto")
Parameters |
|
Notes | It is highly recommended that merge_arrays be ordered right-to-left and bottom-to-top to prevent unexpected merge behaviours or uncaught errors. |
Changing Cell Style
The JavaScript function change_cell_style
helps developers in changing the style
attribute of a table's cells quickly.
change_cell_style(table_name, prop_arrays, is_mirror="auto")
Parameters |
|
Example
By using all the discussed functions in combination, complex placements can be achieved. Let's take the lower portion of the Product Return Records page as an example.
- Let's examine our need regarding the lower section of the page first:
- The 5 rightmost fields to be synced with the rightmost column of the table.
- Other elements can be more arbitrary, but an arrangement with 5 rows seems reasonable enough.
- Create 3 blank tables: Green mirror table, Red blank 5x1 table, and Orange blank 5x12 table
- Insert the 5 fields to be synced into the Red table. The rest of the elements go into the Orange table.
- Put the Red table in the rightmost cell of the Green table. Now we have achieved the first item in (1).
- Put the Orange table in the leftmost cell of the Green table. Then, merge all the cells, except the rightmost one, to get the arbitrary area that we can work with.
- Merge several cells of the Orange table as shown.
- Apply some stylings to certain cells.
- Turn off table border for the finished look.
The final Jinja2 template for this particular example is as follows:
...
<div class="d-none">
<strong id="oldvat_label">Ref. Doc. Values</strong>
<!-- Draw the ORANGE table and the RED table respectively. -->
{{ CuneiTables.blank_table("LowerFormFrameTb", row=5, border=false,
padx=false, pady=false, cell_height="2.15rem") }}
{{ CuneiTables.blank_table("LowerFormFrameTb2", row=5, border=false, col=1,
padx=false, pady=false, cell_height="2.15rem") }}
<!-- Draw the lower form and its fields -->
{{ CuneiForms.general_form_head(lower_form) }}
<!-- Draw all lower form fields here. -->
{{ CuneiForms.general_form_tail(lower_form, populate=false) }}
</div>
<!-- The GREEN table is a mirror table. -->
{{ CuneiTables.draw_mirror_table(body_tb, row=1, border=false, block_width="85vw",
slim=true, padx="0rem", pady="0rem") }}
...
<script type="text/javascript">
// Place fields and labels in the ORANGE table.
stuff_form_to_tb("LowerForm", "LowerFormFrameTb",
[["amt_predisc", -1, -1, 0, 10], ["trddis_note", 1, 10],
["cshdis_note", 2, 10], ["vatrate", 3, 11, 3, 10],
["remark", 1, 0, 0, 0], ["orig_prevat", 2, 7, 1, 7],
["orig_vat", 4, 7, 3, 7], ["net_total", -1, -1, 4, 10]]);
// Place the extra label in the ORANGE table.
stuff_stuff_to_tb(document.getElementById("oldvat_label"),
document.getElementById("LowerFormFrameTb"),
[0,7]);
// Place fields in the RED table.
stuff_form_to_tb("LowerForm", "LowerFormFrameTb2",
[["amt_predisc", 0, 0], ["trddis", 1, 0], ["cshdis", 2, 0],
["vatamt", 3, 0], ["net_total", 4, 0]]);
// Place the ORANGE table in the GREEN table.
stuff_stuff_to_tb(document.getElementById("LowerFormFrameTb"),
document.getElementById("BodyTable_mirror"),
[0,0]);
// Place the RED table in the GREEN table.
stuff_stuff_to_tb(document.getElementById("LowerFormFrameTb2"),
document.getElementById("BodyTable_mirror"),
[0,6]);
// Change cell styles of the ORANGE table.
change_cell_style("LowerFormFrameTb",
[[0, 7, [["border", "1px solid var(--dark)"],
["borderStyle", "solid solid none solid"],
["paddingTop", "0.3rem"]]],
[1, 7, [["border", "1px solid var(--dark)"],
["borderStyle", "none solid none solid"],
["paddingTop", "0.3rem"],
["paddingLeft", "1.5rem"],
["textAlign", "left"]]],
[2, 7, [["border", "1px solid var(--dark)"],
["borderStyle", "none solid none solid"],
["paddingLeft", "0.7rem"],
["paddingRight", "0.7rem"]]],
[3, 7, [["border", "1px solid var(--dark)"],
["borderStyle", "none solid none solid"],
["paddingTop", "0.3rem"],
["paddingLeft", "1.5rem"],
["textAlign", "left"]]],
[4, 7, [["border", "1px solid var(--dark)"],
["borderStyle", "none solid solid solid"],
["paddingLeft", "0.7rem"],
["paddingRight", "0.7rem"],
["paddingBottom", "0.7rem"]]],
[0, 10,[["paddingTop", "0.3rem"],
["paddingLeft", "1rem"],
["textAlign", "left"]]],
[2, 10,[["paddingBottom", "0.3rem"]]],
[0, 0, [["paddingRight", "2rem"],
["paddingLeft", "1rem"],
["textAlign", "left"]]],
[1, 0, [["paddingRight", "1rem"]]],
[3, 10,[["paddingTop", "0.3rem"],
["paddingLeft", "1rem"],
["textAlign", "left"]]],
[4, 10,[["paddingTop", "0.3rem"],
["paddingLeft", "1rem"],
["textAlign", "left"]]], ])
// Change cell styles of the RED table.
change_cell_style("LowerFormFrameTb2",
[[0, 0, [["paddingTop", "0.3rem"]]],
[2, 0, [["paddingBottom", "0.3rem"]]],
[4, 0, [["paddingBottom", "0.7rem"]]], ])
// Merge cells in the GREEN table.
merge_cell("BodyTable_mirror", [[0, 0, 6, 1]]);
// Merge cells in the ORANGE table.
merge_cell("LowerFormFrameTb",
[[0, 10, 2, 1], [1, 10, 2, 1], [2, 10, 2, 1], [4, 10, 2, 1],
[0, 4, 2, 1], [0, 7, 2, 1], [1, 7, 2, 1], [2, 7, 2, 1],
[3, 7, 2, 1], [4, 7, 2, 1], [1, 0, 7, 4], ]);
</script>
Notable Sub-routines
This section aims to give a little insight into some CuneiTable's notable sub-routines.
Live Search
Live search is among the most welcomed feature of CuneiTable. Its inner working is not too complicated, relying mainly on a brute-force approach.
The Illusion of "Live" Search
Being quite resource- and time-intensive, there is a need to avoid unnecessary live-search sequences. At the time of writing, CuneiTable achieves this by putting a 700ms delay between the most recent keystroke (in the table's search box) and the actual searching. This delay is likely here to stay, but the length of the time is subject to adjustment.
Keyword Handling
At the time of writing, CuneiTable's live search handles keywords in the following manner:
- Unless enclosed within a pair of double-quotation marks ("), strings separated by Spaces are treated as multiple keywords.
- Keywords are case-insensitive.
- Valid live search results are entries that contain all keywords within their visible columns, but not necessarily in any single column.
- Values in columns typed as
int
,acc
,qty
,amt
, orpct
are searched in their unformatted forms. - (This stems from CuneiTable's lookup method) One leading space in a keyword can also be interpreted as starting-with.
- (This stems from CuneiTable's lookup method) One trailing space in a keyword can also be interpreted as ending-with.
Selection & Table Tracking
As a Part of Search-and-fill Routine
During the search-and-fill routine, a CuneiTable's role is as follows:
- The search table population routine is triggered when the corresponding search modal is explicitly triggered or when the quick-search routine fails to retrieve exactly one entry. The search modal (a CuneiModal) is shown as a part of the table population process.
- The user makes the desired selection. (See the dedicated #Selection sub-section below for details.)
- Upon confirmation (via ↵ Enter or the modal's button), the search table requests the full result from the server using the
'sel'
fetch mode. (The table itself only holds values of the columns defined in the table's class. The full result, on the other hand, holds full information of the selected entries.) - The table stores the full result in the its selected_entries attribute, then hands the control of the sequence to its modal.
Selection
CuneiTables remember their selection states by storing row indices of the selected entries within their selections attribute. The list below details some notable behaviours:
- For single-selection tables:
- Navigating to an entry (via keyboard or mouse click) automatically implies its selection.
- The record counter shows the index of the selected entry.
- For multi-selection tables:
- Navigating to an entry via keyboard DOES NOT automatically select the entry.
- Clicking on an unselected entry selects it, and clicking on a selected entry deselects it.
- The record counter shows the number of entries being selected.
- It is possible to select/deselect consecutive entries. Note that this feature only works with entries actively shown (not hidden by live search). To select/deselect consecutive entries:
- Click the topmost/bottommost entry to select/deselect.
- Click the bottommost/topmost entry while holding ⇧ Shift.
Table Tracking
Table tracking can be seen as a fast-and-loose version of the search-and-fill routine. While forms that employ search-and-fill have access to the full entries re-retrieved from the server once the selections are confirmed, forms with table-tracking fields only have the data immediately in the table. This eliminates the need of additional requests and creates more streamlined user-experience, at the expense of up-to-date results.
In-line Editing
Form Activation
In CuneiFox, adding/editing a table entry triggers the following sequence:
- For add-type operations: Create a new blank row to accomodate the new entry.
- If the table uses an in-line form, make a request to the server for the default value, and push that value set into data_raw attribute for now.
- If the table employs a full-form, prepare the value to populate the
id
field of the form. This value is fed to the form via the functionactivate_tbform
(Step 3).
- Switch the table's button set from the standard "add-edit-delete" set to show the "OK-cancel" set instead.
- Trigger the editable state of the form.
- Activate the table's form.
- For a table with an in-line form, populate the form with the proper data set from data_raw. Then, switch out the cell texts with form fields hidden in the table's haven element.
- For a table with a full-form, fill the form's
id
field with proper value, trigger a change event on said field, then show the modal. Different values imply different operation as detailed below:- A blank string implies a totally new entry.
- A positive integer implies an edit/delete operation on the entry with this ID.
- A negative integer implies a copy operation on the entry whose ID is the positive value of this integer.
NOTE: A delete operation follows roughly the same procedure, however, with cell-content switching (for in-line forms) and full-form modal showing (for full-forms) skipped. The delete prompt is immediately shown right after the listed steps are completed.
Form Deactivation
Once the add/edit/delete operation is submitted or cancelled, the sequence detailed below is triggered.
- For successful operations: Commit the change to the CuneiTable.
- For add/edit operations: Replace the cell texts (currently residing in the haven element) with the final values as confirmed by the server via
data.entry
. (See Submission of Non-redirecting Form for details.) - For delete operations: Delete the coresponding row from the CuneiTable.
- For add/edit operations: Replace the cell texts (currently residing in the haven element) with the final values as confirmed by the server via
- Switch the table's buttons back to show the standard "add-edit-delete" set.
- For tables in link-mode: Reprocess and rebind the link destination of a row.
- Untrigger the form back to its disabled state.
- Clear all search modals related to the form to prevent search result retention.
- For a table using an in-line form: Switch out the form fields with the cell texts hidden in the table's haven element.
Useful Server-side Patterns
Simple Search Table
@blueprint_name.route("<submit_path>", methods=["POST"])
def function_name(table=None, prefix="ChqSchTable", *kwargs):
if table is None:
req_prefix = request.args.get("prefix", None)
prefix = prefix if req_prefix is None else req_prefix
table = TableClass(prefix=prefix, searchable=True, editable=False)
identifier = json.loads(request.form.get("sel_iden" if request.form.get("mode")=="sel" \
else "identifier"))
# Extract variables and parameters from identifier and/or request.
# ...
# ...
if request.form.get("mode") == "sel":
selected_rows = request.form.get("identifier")
try:
selected_rows = json.loads(selected_rows)
except (TypeError, json.decoder.JSONDecodeError):
return jsonify({"gr_result":"failure"})
# Retrieve all relevant entries from a database.
selected_rows = ...
if len(selected_rows) == 0:
return jsonify({"gr_result":"failure"})
else:
# Re-format the results as a list of dict.
response_data = [ {col0:row0_value0, col1:row0_value1, ...},
{col0:row1_value0, col1:row1_value1, ...},
... ]
return jsonify({"data":response_data, "gr_status":"success"})
if request.form.get("mode") == "qsch" and not(table.multi):
# Try fetching exactly ONE matching result from the server.
# ...
# If the result count is exactly ONE, the quick-search is successful.
if ...:
# Reformat the result as a dict.
return jsonify({"data":qsch_result, "qs_status":"success"})
# Now that the quick-search has failed, grab everything and return.
# ...
# Re-format the results as an array of array.
# The order in each sub-array reflects the table's columns.
response_data = [ [row0_value0, row0_value1, row0_value2, ...],
[row1_value0, row1_value1, row1_value2, ...],
... ]
return jsonify({"data":response_data})
# NOTE THAT this is an extremely simple variation. In practical cases,
# the function 'fetch_all' can retrieve records from the database while
# simultaneously applying the quick-search filter. So, the quick-search
# section might get switched around a bit.
Search Table with a Stand-alone Page
This example is extracted from the route that handles the #Shortcut form below. So, it can seem excessive in places. Feel free to cut and trim when adapting this pattern to your need.
@blueprint_name.route("path_name", methods=["GET", "POST"])
def function_name(ref_tbname=None):
# Handles different references to the table,
# in case some pages need 5 product search modals, for example.
ref_tbname = tbname if ref_tbname is None else ref_tbname
# Check the permission(s).
table.perm_bit = ...
if table.perm_bit == 0:
abort(403)
# FETCH implies the client is merely requesting the data, not the page.
fetch = True if request.args.get("fetch") == "yes" else False
if fetch:
# 'NCOL' FETCH MODE: For new entry in a table with an in-line form.
# This section can be simpler if one doesn't desire the COPY function.
if request.form.get("mode") == "ncol":
identifiers = json.loads(request.form.get("identifier"))
copy_id = request.form.get("copy_id")
# ...
# ...
# Return data in list form. Member order reflects the table's columns.
return_data = [...]
return jsonify({"data":return_data})
# 'GRAB' FETCH MODE: Retrieve a full entry to populate the table's full-form.
elif request.form.get("mode") == "grab":
identifiers = json.loads(request.form.get("identifier"))
thisid = identifiers.get("id", None)
# ...
# ...
# Return data in dict form.
return {...}
# 'SEL' FETCH MODE: Retrieve selection results.
elif request.form.get("mode") == "sel":
selected_rows = request.form.get("identifier")
try:
selected_rows = json.loads(selected_rows)
except (TypeError, json.decoder.JSONDecodeError):
return jsonify({"gr_result":"failure"})
# Retrieve all relevant entries from a database.
selected_rows = ...
if len(selected_rows) == 0:
return jsonify({"gr_result":"failure"})
else:
# Re-format the results as a list of dict.
response_data = [ {col0:row0_value0, col1:row0_value1, ...},
{col0:row1_value0, col1:row1_value1, ...},
... ]
return jsonify({"data":response_data, "gr_status":"success"})
# 'QSCH' FETCH MODE: Retrieve quick-search result OR the whole table.
if request.form.get("mode") == "qsch" and not(table.multi):
# Try fetching exactly ONE matching result from the server.
# ...
# If the result count is exactly ONE, the quick-search is successful.
if ...:
# Reformat the result as a dict.
return jsonify({"data":qsch_result, "qs_status":"success"})
# Now that the quick-search has failed, grab everything and return.
# ...
# Re-format the results as an array of array.
# The order in each sub-array reflects the table's columns.
response_data = [ [row0_value0, row0_value1, row0_value2, ...],
[row1_value0, row1_value1, row1_value2, ...],
... ]
return jsonify({"data":response_data})
# NOTE THAT this is an extremely simple variation. In practical cases,
# the function 'fetch_all' can retrieve records from the database while
# simultaneously applying the quick-search filter. So, the quick-search
# section might get switched around a bit.
# POST method (after the FETCH filter) implies form submission.
if request.method == "POST":
tbform = table.fullform[3] if table.fullform else table.form
# When a database is involved, it is recommended to use 'gen_dbcommit_resp'.
# Many things are taken care of inside.
db_mod_code, new_entry, resp_dict = gen_dbcommit_resp(dbtype, tbname, model,
tbform, table.perm_bit,
suppress_success_msg=True)
if resp_dict == 403:
abort(403)
# If this is not a delete operation, prepare and package resp_dict['entry'] too.
# resp_dict['entry'] is a list of values. Its order reflects the table's columns.
if isinstance(db_mod_code, int) and db_mod_code < 4 and not(new_entry is None):
# ...
resp_dict["entry"] = [...]
return jsonify(resp_dict)
# At this point, it is clear that the client is requesting the stand-alone page.
table.populate_route = url_for("<blueprint>.<function_name>", fetch="yes")
if table.fullform:
table.fullform[3].populate_id = ["id"]
table.fullform[3].populate_route = table.populate_route
# The template 'browse_table.html' is suitable for most stand-alone table page.
# Using it is convenient but not necessary.
return render_template("browse_table.html", ...)
Shortcut
@blueprint_name.route("path_name", methods=["GET", "POST"])
def function_name(mes_dict=None, prefix="DefaultPrefix"):
this_permission = ...
import_form = StdImportForm.gen_import_form("blueprint_name.function_name", upload="yes"))
if request.args.get("upload", False) == "yes" and this_permission >= 5:
# Do import sequence
return ...
if request.args.get("export", False) == "yes" and this_permission >= 1:
# Do export sequence
return ...
real_mes = interpret_mes_request(request.args, 3) if mes_dict is None
else interpret_mes_request(mes_dict, 3)
req_prefix = request.args.get("prefix", None)
prefix = prefix if req_prefix is None else req_prefix
table = TableClass(prefix=prefix, suppress_copy=False, **real_mes)
table.sch_tbs = []
# Append search tables into table.sch_tbs
# ...
dbtype, tbname = "table", "<database_file_name>"
# If the table name and the database name do not match. Also do:
# ref_tbname = "<table_name>"
model, module = CuneiModelClass, "<module_code>"
new_default = function_that_return_default_val_dict
fetch_all = function_that_return_all_entries_in_the_database
return single_tb_page(dbtype, tbname, model, module, table, new_default, fetch_all, request,
import_form=import_form,
export_route=url_for("blueprint_name.function_name", export="yes"),
pin_args=...,
back_route=...,
ref_tbname=ref_tbname) # If necessary.
Future Development
- Pagination: At the moment, CuneiTable works moderately well for entries up to several thousands (depending on the power of the client-unit). However, pagination will allow more entries to be processed and rendered in a swifter manner.
- Change Order-by Column(s): In the present incarnation, the entry order of a CuneiTable is determined from the server. Allowing users to change the ordering from the client-side will definitely boost the usefulness of CuneiTables. However, details regarding entry referencing in the JavaScript code has to be considered.