gltn / stdm

STDM is a free and open source pro-poor land recordation system based on QGIS, PostgreSQL and PostGIS.
http://stdm.gltn.net/
GNU General Public License v2.0
30 stars 30 forks source link

STDM Enhancement: Custom Logic in Entity Editor Forms #344

Closed gkahiu closed 5 years ago

gkahiu commented 6 years ago

Date: 2nd August 2018 Edited: 21st December 2018

Author: John Gitau

Contact: gkahiu@gmail.com

Version: 1.8 (dev)

1. Summary

Currently, STDM provides an automated mechanism for generating a form based on the column types defined for a given entity. The customization of columns provides some basic level of flexibility in defining the properties of each column when adapting the tool to meet the land information requirements for a given context. As much as the current implementation enables rapid design and deployment of data entry forms, it does not always provide the flexibility required to implement ‘intelligent’ forms driven by custom logic.

This proposal highlights the addition of extension points that will enable developers to ‘inject’ custom logic in entity forms to better adapt a form's data capture mechanism to an organization's information requirements. Through this enhancement, it will be possible to:

The implementation will provide the building blocks for a more elaborate framework that will be incorporated in STDM 2.0.

2. Proposed Solution

Add a data_context class decorator function that enables custom classes to extend the logic of entity forms. The data_context function has two mandatory arguments – the profile and entity names of the target entity form i.e. data_context('Informal Settlement', 'Person'). These custom classes will need to be defined under a new ext package, under stdm/ui/forms, and will be loaded dynamically when the plugin is initialized within the QGIS environment.

The primary custom class must inherit from an AbstractEditorExtension class which will contain helper methods for accessing the parent entity form’s attributes.

In a nutshell, the key steps for adding custom form logic will be: i. Add a new module under stdm/ui/forms/ext (the module name does not matter); ii. Create one or more custom classes that inherit from AbstractEditorExtension and are decorated using the data_context function i.e.

@data_context('Informal Settlement', 'Person')
class PersonEditorExtension(AbstractEditorExtension):
    ...

Note: Exceptions in the extension classes will be suppressed and sent to the STDM logger, which should be enabled in the Options dialog.

3. Examples

A few snippets on possible applications of this enhancement:

a. Customize widget properties:

Example: Add a dollar sign to an income field

# stdm/ui/forms/ext/curr_prefix.py
from stdm.ui.forms.editor_extension import (
    data_context,
    AbstractEditorExtension
)

# Person entity in Local Government profile
@data_context('Local Government', 'Person')
Class PersonEditorExtension(AbstractEditorExtension):
    def post_init(self):
        # Override method which is called immediately after the 
        # parent’s __init__ has been executed
        income_widget = self.widget('income')
        # Add dollar sign to show currency
        income_widget.setPrefix('$')

b. Automatically compute a value based on user input:

Example: Compute a household's monetary compensation based on the number of children

# stdm/ui/forms/ext/hh_compensation.py
from stdm.ui.forms.editor_extension import (
    data_context,
    AbstractEditorExtension
)

# Household entity in Informal Settlement profile
@data_context('Informal Settlement', 'Household')
Class HouseholdEditorExtension(AbstractEditorExtension):
    def post_init(self):
        num_child_widget = self.widget('number_of_children')
        # Connect signal to calculate compensation when number of 
        # children changes
        num_child_widget.valueChanged.connect(self.on_num_child_changed)

        # Reference to compensation widget
        self.compensation_txt = self.widget('compensation')

        # Disable user input as value is automatically computed
        self.compensation_txt.setEnabled(False)

    def on_num_child_changed(self, num_children):
        # Slot raised when the number of children changes.
        compensation = self.calc_compensation(num_children)
        self.compensation_txt.setText(compensation)

    def calc_compensation(self, num_children):
        """
        Computes value of compensation based on number of childre.
        :param name: Number of children.
        :type name: int
        :return: Returns the compensation value.
        :rtype: float
        """
        ...

c. Send notifications to the parent editor form:

Example: Limit the number of selected priorities in a multiple select widget

# stdm/ui/forms/ext/farmer_priorities.py
from PyQt4.QtCore import (
    Qt
)

from stdm.ui.forms.editor_extension import (
    data_context,
    AbstractEditorExtension
)

# Farmer entity in Rural Agriculture profile
@data_context('Rural Agriculture', 'Farmer')
Class FarmerEditorExtension(AbstractEditorExtension):
    def post_init(self):
        self.priorities_widget = self.widget('priorities')
        p_model = self.priorities_widget.item_model

        # Connect signal when a priority is (un)checked
        p_model.itemChanged.connect(self.on_priority_changed)

    def on_priority_changed(self, p_item):
        # Slot raised a priority is checked or unchecked. Limit to 3.
        if len(self.priorities_widget.selection()) > 3:
            # Uncheck the priority item
            p_item.setCheckState(Qt.Unchecked)

            # Insert notification in parent editor
            msg = 'Maximum number of allowed priorities is 3.'
            self.insert_warning_notification(msg)

d. Incorporating custom validation before saving the form data:

Example: Validate a telephone number before saving

# stdm/ui/forms/ext/person_tel.py
from stdm.ui.forms.editor_extension import (
    data_context,
    AbstractEditorExtension
)

# Person entity in Local Government profile
@data_context('Local Government', 'Person')
Class PersonEditorExtension(AbstractEditorExtension):
    def post_init(self):
        self.tel_widget = self.widget('telephone')

    def is_telephone_valid(self, tel_num):
        # Custom logic to validate number
        ...

    def validate(self):
        # Override base class method to incorporate custom validation logic. 
        # It must return True or False
        tel_num = self.tel_widget.text()
        is_valid = self.is_telephone_valid(tel_num)
        if not is_valid:
            self.insert_warning_notification('Invalid telephone number')

        return is_valid

e. Specify conditional visibility of widgets based on user input:

Example: Enable widget for specifying a spouse's name if 'marital status' is 'Married'

# stdm/ui/forms/ext/person_spouse_name.py
from stdm.ui.forms.editor_extension import (
    data_context,
    AbstractEditorExtension
)

# Person entity in Local Government profile
@data_context('Local Government', 'Person')
Class PersonEditorExtension(AbstractEditorExtension):
    def post_init(self):
        self.m_status_cbo = self.widget('marital_status')
        self.m_status_cbo.currentIndexChanged.connect(
            self.on_m_status_changed
        )
        self.spouse_name_txt = self.widget('spouse_name')

    def on_m_status_changed(self, m_status):
        # Enable/disable spouse name text widget based on marital status
        if m_status == 'Married':
            self.spouse_name_txt.setVisible(True)
        else:
            self.spouse_name_txt.clear()
            self.spouse_name_txt.setVisible(False)

f. Define cascading comboboxes:

Example: Filter roof materials based on the type selected as shown in the table below. The codes in brackets correspond to those defined in STDM when defining lookup values. The codes are mandatory in order for the cascade configuration to work.

Roof Type Material
Metal (M) Tin (TN), copper (CP), zinc (ZC), aluminium (AL), steel (ST)
Bituminous (B) Fiberglass (FG), cellulose (CL)
# stdm/ui/forms/ext/house_roof_type.py
from stdm.ui.forms.editor_extension import (
    cascading_field_ctx,
    data_context,
    AbstractEditorExtension
)

# House entity in rural agriculture profile
@data_context('Rural Agriculture', 'House')
Class HouseEditorExtension(AbstractEditorExtension):
    def post_init(self):
        # Filter roof materials based on the roof type selected
        # Codes for lookup values SHOULD have been specified
        roof_type_cf_ctx = cascading_field_ctx(
            'roof_type',
            'roof_material',
            ['M', 'B'],
            [
                ('TN', 'CP', 'ZC', 'AL', 'ST'),
                ('FG', 'CL')
            ]
        )

        # Add context to the editor extension
        self.add_cascading_field_context(roof_type_cf_ctx)

4. Affected Files

A new module, editor_extension under ui/forms, will be created. It will contain the following functions and classes:

stdm custom logic in edtor forms - uml class diagram 31 08 2018-1

In addition, the following existing classes will be updated to accommodate this feature:

5. Status

Enhancement has been merged in master 4c03761.

pgathogo commented 6 years ago

This enhancement is very important and will greatly improve the behavior of forms when capturing data. However, I have one question, in example F - Cascading comboboxes, why do I have to add cascading context to the cascading manager explicitly?

self.add_cascading_field_context(roof_type_context)

Another thing to remember, users should be told if they import data using the import module, then they will miss out on the validation logic in the forms.

gkahiu commented 6 years ago

No, the context will not be added explicitly to the manager but rather implicitly i.e. the class AbstractEditorExtension will create the manager internally such that when you call the function add_cascading_field_context then the cascade manager will internally add the context and manage the collection.

Regarding the import, you are right. Some form of validation will be lost from the validation logic. For version 2.0, we will need a central point for managing validation that can be adopted across the whole application.

pgathogo commented 6 years ago

I think I understand it now, the cascade manager is the one in charge of executing the cascading behavior while the CascadingFieldContext contains the mapping.

gkahiu commented 6 years ago

Exactly.

On Sat, 1 Sep 2018 at 12:39, pgathogo notifications@github.com wrote:

I think I understand it now, the cascade manager is the one in charge of executing the cascading behavior while the CascadingFieldContext contains the mapping.

— You are receiving this because you were assigned. Reply to this email directly, view it on GitHub https://github.com/gltn/stdm/issues/344#issuecomment-417846682, or mute the thread https://github.com/notifications/unsubscribe-auth/ADqZC77AXTLSXxZf4bDFg4TFl1JQHXYEks5uWlXFgaJpZM4WVE1_ .

-- John Gitau

wondie commented 6 years ago

@gkahiu this is a nice enhancement! I have two questions. Is it profile.name or display name also is it entity name or short name under data_context? If they are names then I think they don't need to have space.

Is the code transportable? Let us say, I wrote a logic on my STDM installation. How can other people access this same logic?

gkahiu commented 6 years ago

Hi Wondie,

Regarding the names, the profile name corresponds to profile.name, I believe you can use insert spaces for profile names. The entity name corresponds to the short_name.

Regarding the sharing of custom form extensions, currently there is no elegant way of doing this apart from the basic 'copy-and-paste' of the contents under the ext folder but it can be packaged in an installer if building a custom solution for specific partners. However, I have been thinking of having a resource sharing framework for version 2.0 where users can be able to share, discover and download configurations/profiles, document templates, extensions etc. That said, I do welcome suggestions on options, that can be quickly implemented, for sharing form extensions.

Best regards,

John

On Mon, Sep 3, 2018 at 5:20 AM Wondimagegn Tesfaye notifications@github.com wrote:

@gkahiu https://github.com/gkahiu this is a nice enhancement! I have two questions. Is it profile.name or display name also is it entity name or short name under data_context? If they are names then I think they don't need to have space.

Is the code transportable? Let us say, I wrote a logic on my STDM installation. How can other people access this same logic?

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/gltn/stdm/issues/344#issuecomment-417982680, or mute the thread https://github.com/notifications/unsubscribe-auth/ADqZC2NNgHcnpHcDlf47o1thi_AJm-7Eks5uXJHdgaJpZM4WVE1_ .