Conditional creation of notebooks in views

Hi, is there a way in an inherited view to add a page to a <notebook> if one already exists, or create the notebook with the page if it doesn’t?

Context: I have several independent modules that extend the employee form (company.employee). Each module adds a tab (Holidays, Documents…), but they don’t depend on each other. The problem:

  • If module A creates a <notebook> after //field[@name='supervisor'] and module B does the same, I get two separate notebooks stacked vertically.

  • If module B targets //notebook with position="inside", it fails when module A is not installed because there’s no notebook to target.

Is there a pattern for “add to existing notebook, or create one if none exists”? Or is the convention to have one module always create the notebook and the others depend on it?

Regards

For now I do not think it is possible to do what you want because we have an assert to ensure that XPath expression found at least one element.
I think it could be an improvement if we allow to mark some XPath expression as optional and so they may not match. But to be able to do what you want we will also need to define and “else”-case or you will need two optional XPath.

Thank for the hint. I checked the code and I think this is the part you are talking about

/ir/ui/view.py

@classmethod
def inherit_apply(cls, tree, inherit):
    root_inherit = inherit.getroottree().getroot()
    for element in root_inherit:
        expr = element.get('expr')
        targets = tree.xpath(expr)
        assert targets, "No elements found for expression %r" % expr  # <------
        for target in targets:
            position = element.get('position', 'inside')
            new_tree = getattr(cls, '_inherit_apply_%s' % position)(
                tree, element, target)
            if new_tree is not None:
                tree = new_tree
    return tree

if I add this code

@classmethod
def inherit_apply(cls, tree, inherit):
    root_inherit = inherit.getroottree().getroot()
    for element in root_inherit:
        expr = element.get('expr')
        targets = tree.xpath(expr)
        
        # NEW: if no targets and a create_expr is specified,
        # create the container
        if not targets and element.get('create_expr'):
            create_expr = element.get('create_expr')
            create_position = element.get('create_position', 'after')
            create_targets = tree.xpath(create_expr)
            if create_targets:
                # Build the container (e.g., <notebook>)
                # and add element's children inside it
                container = element[0]  # first child is the container
                getattr(cls, '_inherit_apply_%s' % create_position)(
                    tree, container, create_targets[0])
                # Now the original xpath should find the container
                targets = tree.xpath(expr)
         # END NEW CODE
        
        assert targets, "No elements found for expression %r" % expr
        for target in targets:
            position = element.get('position', 'inside')
            new_tree = getattr(cls, '_inherit_apply_%s' % position)(
                tree, element, target)
            if new_tree is not None:
                tree = new_tree
    return tree

I can inherit the view as follow

<!-- document_hr/view/employee_form_documents.xml -->
<data>
    <xpath expr="//notebook" position="inside"
           create_expr="//field[@name='supervisor']" create_position="after">
        <notebook colspan="4">
            <page string="Documents" id="page_documents">
                <field name="documents" colspan="4"/>
            </page>
        </notebook>
    </xpath>
</data>

with these new elements,

create_expr - where to create the content if `expr` finds nothing
create_position - How to insert at the fallback (after, before, inside)

I don’t know if this make sense, add value, or it is align with the Tryton design principles.
As an alternative I was thinking to create a small module employee_notebook that create the notebook and the rest of modules depend on it… but I am not sure that create a module only to create a notebook in a view is maybe too much.

any feedback or advice?

create_ attribute looks like adhoc solution to your specific problem.
I think it will be better to use an optional flag like:

<xpath expr="/form" position="inside" unless="/form/notebook">
   <notebook colspan="4"></notebook>
</xpath>
<xpath expr="//notebook" position="inside">
   <page></page>
</xpath>
1 Like

you are right, doing in 2 steps simplify the solution and it is more readable.

so If I am not mistaken is just this (+ xml validations),

# trytond/ir/ui/view.py — inherit_apply method

@classmethod
def inherit_apply(cls, tree, inherit):
    root_inherit = inherit.getroottree().getroot()
    for element in root_inherit:
        expr = element.get('expr')

        # NEW: skip this xpath if 'unless' condition matches
        unless = element.get('unless')
        if unless and tree.xpath(unless):
            continue
       # END NEW

        targets = tree.xpath(expr)
        assert targets, "No elements found for expression %r" % expr
        for target in targets:
            position = element.get('position', 'inside')
            new_tree = getattr(cls, '_inherit_apply_%s' % position)(
                tree, element, target)
            if new_tree is not None:
                tree = new_tree
    return tree

Do you think this adds value to the framework? In my humble opinion, yes — for the following reasons:

  1. It completes the view composition model. Tryton already supports adding, removing, replacing, and modifying elements. But there’s no way to say “add this container only if it doesn’t exist yet.” I think that’s a fundamental pattern in modular UI composition.
  2. It enables healthier third-party module ecosystems. Explicit coordination between modules works well within Tryton’s core, but doesn’t scale when combining modules from different authors (in my case, I’m integrating NaN-tic modules alongside custom modules). The unless attribute removes the need for artificial dependencies between functionally unrelated modules.

Feedback?

1 Like

I think it can be added in core but I would also implement for completeness an if attribute.

1 Like

So, if I understood properly, the full functionality should be like this

  • unless="xpath" - skip this element if the xpath matches (prevents duplicates)
  • if="xpath" - skip this element if the xpath does NOT match (optional modifications)

if so, the implementation may be

# trytond/ir/ui/view.py — inherit_apply method

@classmethod
def inherit_apply(cls, tree, inherit):
    root_inherit = inherit.getroottree().getroot()
    for element in root_inherit:
        expr = element.get('expr')
         
        # NEW
        # Skip this xpath if 'unless' condition matches
        unless = element.get('unless')
        if unless and tree.xpath(unless):
            continue

        # Skip this xpath if 'if' condition does NOT match
        if_ = element.get('if')
        if if_ and not tree.xpath(if_):
            continue

       # END NEW

        targets = tree.xpath(expr)
        assert targets, "No elements found for expression %r" % expr
        for target in targets:
            position = element.get('position', 'inside')
            new_tree = getattr(cls, '_inherit_apply_%s' % position)(
                tree, element, target)
            if new_tree is not None:
                tree = new_tree
    return tree

and this is how use the if condition

<!-- The new module wants to add a field to the other module page, but only if other_module is installed -->
<xpath expr="//page[@id='other_module_page']" position="inside" if="//page[@id='other_module_page']">
    <field name="my_extra_field"/>
</xpath>

am I correct?

1 Like

That’s correct but the if is only useful if it applies at another element than the one in expr as if expr doesn’t match then we wouldn’t apply the change anyway.

No the if attribute makes the XPath optional otherwise Tryton will complain about the missing element.