CuneiForm

จาก คูนิฟ็อกซ์ วิกิ

CuneiForm type objects are powered by WTForms engine. However, this object type also introduce a handful of additional variables and attributes to strike a sweet balance between ease of development and flexibility.

Define

Defining a CuneiForm is essentially similar to declaring a WTForm. There are no differences in declaring fields, validations, render keywords, and validating functions. (There exist a few special functions that can dynamically generate some common fields, but we'll leave that to a later section.)

Let's first have a quick look at a very basic CuneiForm:

class LoginForm(FlaskForm, CuneiForm):
    company = StringField("Company", validators=[InputRequired("Required!")])
    username = StringField("Username", validators=[InputRequired("Required!")])
    password = PasswordField("Password", validators=[InputRequired("Required!")])
    clear_session = BoolField("Log-out of other sessions", default=True)
    submit = SubmitField("Log-in")

    def validate_company(self, company):
        # Validation goes here
    def validate_username(self, username):
        # Validation goes here
    def validate_password(self, password):
        # Validation goes here

Developers might find uses of BoolField and IntField in CuneiFox code base unfamiliar. This convention is to avoid name conflict betweem WTForm and Peewee.

Special Value Types

This feature plays a crucial role in ensuring consistent value formatting for client-side output. While it originated as a solution to address browser formatting inconsistencies, it has evolved to serve several other purposes within the CuneiFox framework.

Original Intentions

Reading through the CuneiFox code base, one is sure to notice the excessive use of StringFields where one would expect DateFields, TimeFields, FloatFields, and IntegerFields as supported by WTForms. This design choice stems was made to address a challenging browser behaviour. Modern browsers tend to enforce formatting of fields marked as certain types based on the user's locale setting, which can be tied to either the operating system or the browser itself. This automatic formatting can be difficult to override or reverse, leading to inconsistencies and confusion on the user's part especially when switching workstations.

To address this issue, CuneiFox developed its own value typing. Our approach applies custom-made JavaScript codes on unformatted and untyped (as far as the browser is aware) fields. This way, the formatting rendered on the client-side perfectly obeys the company's specific preferences within the CuneiFox system.

Current Incarnation

Form value types are specified in the variable MASTER_sp_type residing within form definition code. The variable is of format:

MASTER_sptypes = [(str field_name0, str type0), (str field_name1, str type1), ...]

# Example from Accounting Journal's withholding tax form
MASTER_sptypes = [("taxdate", "date"), ("taxmonth", "month"),
                  ("rate", "pct"), ("amt", "acc"), ("tax", "acc"),
                  ("rate_2", "pct"), ("amt_2", "acc"), ("tax_2", "acc"),
                  ...]
# Example from Product information form
MASTER_sptypes = [...,
                  ("pict", ["img", "table", "product"])
                  ...]

Available Types

Special types valid within CuneiFox system are shown below:

  • Date types
    • 'date': Value with date, month, and year.
    • 'month': Value with only date and month.
  • Time types
    • 'time': Value with hour, minute, and second.
  • Numeric types
    • 'acc': Numerical value, rounded to acc decimal places. (See Company Settings)
    • 'pct': Numerical value, rounded to pct decimal places, with "%" sign appended. (See Company Settings)
    • 'qty': Numerical value, rounded to qty decimal places. (See Company Settings)
    • 'amt': Numerical value, rounded to amt decimal places. (See Company Settings)
    • 'int': Numerical value, rounded to nearest integer.
  • File types* (See note below)
    • 'file': File field with CuneiFox custom action buttons.
    • 'img': File field with CuneiFox custom action buttons and an associated img HTML element.
  • Other types
    • 'right': Normal string field whose text is aligned right rather than the normal left.
    • 'color': String field with an associated color selector element.

NOTE THAT when using file types, the typeX value is a 3-membered list instead:

  • Member 0: The type ('file' or 'img')
  • Member 1: The source ('data', 'table', 'files', 'print', 'qr', 'report')
  • Member 2: The sub-directory (string or None)

Instant Calculation

Instant calculation function within CuneiForms, 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 under form definition.

MASTER_instacalc = [(str trigger_fld0, [
                                           route0, 
                                           [str collect_fld00, str collect_fld01, ...],
                                           [str result_fld00, str result_fld01, ...]]),
                                           ...
                                       ]),
                   [(str trigger_fld1, [
                                           route1, 
                                           [str collect_fld10, str collect_fld11, ...],
                                           [str result_fld10, str result_fld11, ...]]),
                                           ...
                                       ]),
                    ...]

# Example from Accounting Journal's withholding tax form
MASTER_instacalc = [("amt", ["cunei_gl.calcwht", ["amt", "rate"], ["tax"]]),
                    ("rate", ["cunei_gl.calcwht", ["amt", "rate"], ["tax"]]),
                    ...]
  • trigger_fldX is the field whose value change will trigger the instacalc routine.
  • routeX can be either:
    • A string similar to the string argument fed to Flask's url_for.
    • A dict of format {'route': str route_name, **kwargs}, where kwargs represents arguments to be fed to the instacalc route.
  • collect_fldXY are the fields whose values are collected and used in calculation routine.
  • result_fldXY are the fields to be updated with the calculation result.

Auto-numbering Function

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 in this format:

MASTER_autorun = [(str run_field0, str sec_field0, str date_field0, str ref_field0),
                  (str run_field1, str sec_field1, str date_field1, str ref_field1),
                  ...]

# Example from Sale Invoice Form
MASTER_ autorun = [("docno", "section_stid", "docdate", None),
                   ("invno", "section_stid", "invdate", "docno")]
  • run_fieldX is the field to trigger the auto-numbering function.
  • sec_fieldX is the field holding the corresponding section static ID.
  • date_fieldX is the field holding the corresponding date.
  • ref_fieldX is the field holding the original value for WAIT function (Set to None to disable WAIT support).

Other Form 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_skipseqs: A list of field names to be skipped when the user is navigating through the form via Tab ↹ or ↵ Enter.
  • MASTER_skipcols: A list of field names to be skipped when committing data to database. CuneiFox naturally skip fields that are meant for client-side viewing only (fields whose names are not present as database table columns). However, if the developer wants a field to be skipped despite the database having a similarly named column, do put the field name in this list.

These 3 variables share the same format:

MASTER_firstonly = [str field_name0, str field_name1, ...]

# Example from Accounting Journal's header form
MASTER_firstonly = ["docno", "section_stid", "section_code", "section_name"]

Initiate

Form initiation create an object instance for a CuneiForm. It should be noted that CuneiFox does not override the original __init__ of Flask_WTF's FlaskForm, but has written its own init_form class method which calls the original __init__ and performs other facilitating routines.

init_form(cl, prefix="", form_name="", gen_del=None, has_files=False, **kwargs)

Parameters
  • prefix (str): This value is reflected in FlaskForm's _prefix and CuneiForm's _id attributes during initiation. The _prefix attribute is not directly accessed if one follows the usual coding pattern of CuneiFox.
  • form_name (str or LazyString): The human-readable form name. This is very rarely accessed (examples include Permission forms and Setting Forms which require the form names to be explicitly and dynamically rendered on the client-side) and usually omitted.
  • gen_del: The module to store, at runtime, the parallel CuneiForm class with generic submit-delete field set. (See description under Section: Submit-delete Field Set.
  • has_files (bool): Whether the form contains one or more fields for files. (This has most likely been made obsolete and is always omitted.)
  • Other keyword arguments: See notes under sections below.
Returns A new FlaskForm-CuneiFox object.

Submit-delete Field Set

The way CuneiFox handles data input and deletion requires fields named submit (SubmitField), del_submit (SubmitField), and is_del (BooleanField) to be present in most forms. So, CuneiFox allows developers to define a form with these 3 fields omitted, then provide a not-None gen_del argument upon initiation.

The way CuneiFox goes about this is to create a parallel FlaskForm-CuneiForm class with these 3 fields added, then store the new class in the module specified in the argument for repeated use. The gen_del argument can be:

  • str module_code: In this case, runtime class is stored within the module's 'forms' sub-module.
  • [str module_code, str submodule_name]: In this case, the new class is stored in the specified sub-module.

NOTE THAT this feature does not override any similarly named field in the original form definition. For example, if the definition of the form already contains a 'submit' field, only 'del_submit' and 'is_del' fields are injected.

Notes on Keyword Arguments

MASTER-adjacents

Back to the #Define section, we are introduced to a number of MASTER_xxx variables. Those variables set at form definition act as the default values for each form type. However, developers can feed an overriding value that applies only to the form being initiated using 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:

  • skipseqs, skipcols, firstonly, and autorun are normal form attributes and can easily be accessed and modified.
  • instacalc are repacked as a dict upon initiation. The format and an example of post-init modification are shown below:
form_obj.instacalc = {str trigger_fld0: [
                                           url_for_string route0, 
                                           [str collect_fld00, str collect_fld01, ...],
                                           [str result_fld00, str result_fld01, ...]]),
                                           ...
                                        ]),
                      str trigger_fld1: [
                                           url_for_string route1, 
                                           [str collect_fld10, str collect_fld11, ...],
                                           [str result_fld10, str result_fld11, ...]]),
                                           ...
                                        ]),
                      ...]

# Example from Product Return / Cancelled Order header form
head_form.instacalc["refdoc"]  = [
                                    url_for("cunei_iv.fetch_vouch_for_return", bookcode=bookcode),
                                    ["refdoc", "refdate", "docdate"],
                                    ["company_stid", "company_code", "company_name", ...]
                                 ]
head_form.instacalc["refdate"] = [
                                    url_for("cunei_iv.fetch_vouch_for_return", bookcode=bookcode),
                                    ["refdoc", "refdate", "docdate"],
                                    ["company_stid", "company_code", "company_name", ...]]
  • sptypes is distributed and packed as field attributes. We will follow-up on the example given under #Special Value Types section. After initiation, special value specifications are thus set:
taxform_obj.taxdate.special_type = "date"
taxform_obj.taxmonth.special_type = "month"
tax_form_obj.rate.special_type = "pct"
tax_form_obj.amt.special_type = "acc"
tax_form_obj.tax.special_type = "acc"
tax_form_obj.rate_2.special_type = "pct"
tax_form_obj.amt_2.special_type = "acc"
tax_form_obj.tax_2.special_type = "acc"

# For file types, values are thus packed:
product_form_obj.pict.special_type = "img"
product_form_obj.pict.file_src_type = "table"
product_form_obj.pict.file_src_dir = "product"

Know Thy Self

On the client-side, forms are going to be present, possibly, on a page along with other forms, tables, and modals. In many cases, they might even have to interact with them. So, CuneiForms need a way to identify themselves and other elements they are meant to work closely with. Below is the list of form attributes whose functions fall in this category:

  • _id (str): The 'prefix' repacked. This value will become the HTML id of the form object. (Note that the attribute name is underscored to avoid conflict with python-native 'id'.)
  • in_table (str): The name of the table with this form as the in-line form. (The attribute defaults to False for free-standing forms.)
  • table_linked (str): Similar to in_table but this one applies to full pop-up form (as opposed to in-line form).
  • in_modal (str): the ID of the modal with this form as the MAIN SUBMITTING COMPONENT. (The attribute defaults to False for free-standing forms.)
  • sequence_bound (bool: defaults to False): Whether form submission is linked to advance the edit-mode of the page. (See Multi-component Page)
  • slim (bool: defaults to False): Whether to render the form in slim mode. (Normally, form label and field take 2 'rows' on the screen. In slim mode, form label and field share the same 'row', and the field is drawn with less vertical padding.)
  • subslim (bool: default is determined upon CuneiTable initiation): Similar to slim but applicable ONLY TO in-line fields of an in-line form. (E.g. fields in an expansion pop-up are not governed by this attribute.)

Sequence and Column Names

These few attributes govern the navigation, submission, and database-mapping of the form:

  • seq ([str,]): Field names as defined via FlaskForm. Its order dictates the tab-sequence of the form on the client-side. If no value is given, the initiation routine automatically generates this list from FlaskForm.
  • cnames ([str,]): Field names as matched with corresponding database model. If no value is given, the list copies from seq. (See notes.)
  • submit_id (str: defaults to 'submit'):

The lists seq and cnames work together to create a mapping key. In other words, data entered through the form field seq[i] is written to database column cnames[i] and vice-versa for reading operations. DO NOTE THAT, under most use cases, seq and cnames both hold the same values. Assigning different values to seq and cnames is legitimate in some use cases. However, the rarity of such cases can make the code harder to parse by future developers. A more recommended way to achieve similar result is to explicitly assign values to appropriate columns/keys during data manipulation.

Getting Data

By default, a CuneiForm only displays either the default values (as specified during form definition) or the values fed explicitly before the page is sent. Once the page components are loaded, the form can then ask for data from the server in a separate HTML request. These attributes below dictate the form's behaviours regarding data request:

  • populate_route (url_for_string)
  • populate_suppress ([str,])
  • populate_id ([str,])

Sending Data

Normal submission behaviour of a FlaskForm is to send a POST request to the same currently displaying route, then the page is refreshed. This approach, although neat and clean, creates a choppy experience for users. CuneiFox offers a way to override this behaviour using the following attributes:

  • post_route (url_for_string)
  • redirect (bool)

Sometimes, a form submission is followed by a long, thread-based operation. More attributes are needed to dictate the thread-tracking behaviours.

  • wait_long_process (bool)
  • track_route (url_for_string)
  • wait_redirect (url_for_string)

Design & Pre-made Scripts

Notable Sub-routines