inveniosoftware-attic / invenio-circulation-legacy

https://invenio-circulation.readthedocs.io
GNU General Public License v2.0
4 stars 8 forks source link

RFC: programming approach #3

Open mvesper opened 9 years ago

mvesper commented 9 years ago

RFC Programming approach

The circulation module is going to be rewritten from scratch in Invenio 2, making it more flexible and covering more library use cases. The purpose of this RFC is to discuss current development stages regarding the programming approach.

While discussing the possible use cases for the circulation module, it became apparent that the module is basically about managing states of different entities and triggering events based on those changes. Along with the fact that the statuses of items vary among the instances managing a library, the following approach is proposed:

The two entities having varying states, Item and Request (or LoanCycle, naming to be discussed), would be managed by using a configuration file using the following pattern:

{
    "on_shelf":
        {
            "from": 
                [
                    {
                        "status": "on_loan",
                        "attempt_enter":
                            {
                                "function": attempt_enter_shelf_func,
                                "needed_attributes": []
                            },
                        "on_enter": enter_shelf_func

                    }
                ],
            "to":
                [
                    {
                        "status": "on_loan",
                        "action_name": "loan",
                        "attempt_leave":
                            {
                                "function": attempt_leave_shelf_func,
                                "needed_attributes": []
                            },
                        "on_leave": leave_shelf_func,

                    }
                ]
        },
    "on_loan":
        {
            "from": 
                [
                    {
                        "status": "on_shelf",
                        "attempt_enter":
                            {
                                "function": attempt_enter_loan_func,
                                "needed_attributes": []
                            },
                        "on_enter": enter_loan_func

                    }
                ],
            "to":
                [
                    {
                        "status": "on_shelf",
                        "action_name": "return",
                        "attempt_leave":
                            {
                                "function": attempt_leave_loan_func,
                                "needed_attributes": []
                            },
                        "on_leave": leave_loan_func,

                    }
                ]
        }
}

This configuration file defines every possible transition between statuses and defines what is supposed to happen during those changes.

Example: Switching from "on_shelf" to "on_loan" While being in status "on_shelf", one possible transition is to go to the status "on_loan". When this transition is triggered, the following functions are executed:

The classes Item and Request would then provide a very generic interface like the following:

class Item/Request(object):
    @classmethod
    def new(cls, ...):
        """Creates a new entity."""

    @classmethod
    def get(cls, id):
        """Gets and entity by ID."""

    @classmethod
    def get_all_functions(cls):
        """Gets all available functions triggering a transition."""

    @classmethod
    def get_all_statuses(cls):
        """Gets all available statuses."""

    def run(self, function_name, **kwargs):
        """Runs a transition."""

    def get_available_functions(self):
        """Gets the available functions triggering a transition from the current status."""

Following this approach the needed statuses and workflows can be configured and the demands for logging events can be satisfied.

UPDATE 18.05.2015

After playing around with that approach and the presented configuration file it became apparent that the config approach is a little to elaborate. It basically led to a lot of redundancy and was therefore hard to read. For example the loan action, would require a section in "on_shelf" -> "to" -> "on_loan" as well as a section in "on_loan" -> "from" -> "on_shelf". Additionally, this redundancy provided pretty little value. Sticking to the loan action example: The approach would trigger four function calls, and the question arose: Is this really necessary?

So, even though it moves a little bit away from the "state machine approach", this new configuration file style is proposed:

{
    "on_shelf":
        {
            "loan":
                {
                    "new_status": "on_loan",
                    "parameters": {"user": user.User},
                    "validate_func": validate_loan_item,
                    "func": loan_item
                },
            "lose_from_shelf":
                {
                    "new_status": "missing",
                    "parameters": {},
                    "validate_func": validate_lose,
                    "func": lose
                },
            "request":
                {
                    "new_status": "on_shelf",
                    "parameters": {"user": user.User,
                                   "date": datetime.datetime},
                    "validate_func": validate_request,
                    "func": request
                }
        },
    "on_loan":
        {
            "return":
                {
                    "new_status": "on_shelf",
                    "parameters": {},
                    "validate_func": validate_return_item,
                    "func": return_item 
                },
            "lose_from_loan":
                {
                    "new_status": "missing",
                    "parameters": {},
                    "validate_func": validate_lose,
                    "func": lose
                },
            "request":
                {
                    "new_status": "on_loan",
                    "parameters": {"user": user.User,
                                   "date": datetime.datetime},
                    "validate_func": validate_request,
                    "func": request
                }
        },
    "missing":
        {
            "return":
                {
                    "new_status": "on_shelf",
                    "parameters": {},
                    "validate_func": validate_return_missing,
                    "func": return_missing
                }
        }
}

It's a more direct mapping from a given status to the available actions related to it. The _validatefunc and func parameter work the same way as the "attempt" -> "function" and "on" functions in the previous configuration, whereas parameters corresponds to "needed_attributes", but moved up one level, since they are needed for both function calls anyways.

Also the interface changed a bit (Should become more similar to the Invenio 2 way):

class CirculationObject(object):

    @classmethod
    def new(cls, status='new'):
        """Creates a new entity."""

    @classmethod
    def get(cls, id):
        """Gets an entity by ID."""

    @classmethod
    def query(cls, **kwargs):
        """Gets entities by utilizing the provides search capabilities."""

    @classmethod
    def get_all_functions(cls):
         """Gets all available functions triggering a transition."""

    @classmethod
    def get_all_statuses(cls):
        """Gets all available statuses."""

    def get_function_parameters(self, function_name, status=None):
        """Returns the required parameters for a given function."""

    def validate_run(self, function_name, **kwargs):
        """Runs the *validate_func* function."""

    def run(self, function_name, **kwargs):
        """Runs a transition."""

    def get_available_functions(self):
        """Gets the available functions triggering a transition from the current status."""

UPDATE 17.07.2015

As it turned out, a function based approach causes some difficulties regarding the validation, since some of the information gathered during the validation step can be reused in the actual run. Therefore, objects are proposed to store those kind of information.

item_config = {
    "on_shelf":
        {
            "loan": LoanConfig,
            "lose_from_shelf": LoseConfig,
            "request": RequestConfig
        },
    "on_loan":
        {
            "return": ReturnConfig,
            "lose_from_loan": LoseConfig,
            "request": RequestConfig
        },
    "missing":
        {
            "return": ReturnMissingConfig
        }
}

An example Config object looks like this:

class ReturnConfig(ConfigBase):
    new_status = 'on_shelf'
    parameters = ['item']

    def run(self, _storage, item=None, **kwargs):
        clc = CirculationLoanCycle.search(item=item,
                                          current_status='on_loan')[0]
        clc.run('return', _storage=_storage)

    def check_item(self, item=None, **kwargs):
        if not item:
            raise Exception('There needs to be an item')

The _newstatus attributes triggers the status change after execution of the script, while parameters is supposed to make the interaction easier.

There is currently also some magic happening, which executes every method starting with 'check' during the validation step and then gathers the exceptions. This is supposed to be utilized later on in the user interface to provide information for the user about incorrect settings. But there should be a debug mode for developers, since this exception gathering removes some information currently.

david-caro commented 8 years ago

Is this still relevant?