Skip to content

4.4 step7

Jean Cavallo edited this page Jan 17, 2019 · 4 revisions

Step 7 - Advanced Dynamicity

We just saw how to add dynamicity by using states and domains. Tryton provides other tools to make everything "feel right". For instance, something that probably bothered you (until now) was that Function fields were not updated / set when creating or modifying a record. For instance, the latest_exemplary field could be updated as soon as we add or modify the exemplaries list, but for now it is not. Let's fix this!

Defaults

The first thing that can be done for a better UI is to set "logical" default values. For instance, we may assume that when a new exemplary is created, its acquisition_date would be the current date. Add the following method under library.book.exemplary:

@classmethod
def default_acquisition_date(cls):
    return datetime.date.today()

Go back in the application, then create a new exemplary of a book. You will notice that the acquisition_date field is set by default with the current date.

That is how tryton manages default value: by looking for a method on the model named default_<field_name>. If this method is found, it will be called and its value set as the default value for the field.

There are a few things that can be noted here:

  • The method is a classmethod. This means that you cannot make the default value of a field depend on that of another field. There are way to manage this behavior, but not using the default_ methods.

  • Default values are set in two cases:

    • When using the client, if the field is displayed in the view, the default method will be called automatically, and its value will be set in the view client side

    • When a new instance is saved server side, for which the field was not given any value (an empty value, set through my_instance.my_field = None, does count as a "given" value), the server will automatically call the default_ method, and set its return value before saving

Default methods are also used to initialize new columns in the database if possible. For instance, if you add a new boolean field, with a default_<my_field> returning True, the column that will be created will have the True value in existing records in the database.

As for Function fields, default methods are expected to return:

  • An id for Many2One fields
  • A list of ids for Many2Many fields
  • A list of dictionaries for One2Many fields
  • The value for all other types

On Change With

What we are going to talk about now is fully UI oriented. Meaning, there will be no impact whatsoever on the server behavior.

Add the following method in library.author:

@fields.depends('birth_date', 'death_date')
def on_change_with_age(self):
    if not self.birth_date:
        return None
    end_date = self.death_date or datetime.date.today()
    age = end_date.year - self.birth_date.year
    if (end_date.month, end_date.day) < (
            self.birth_date.month, self.birth_date.day):
        age -= 1
    return age

If the code contents ring a bell, that is normal, we will talk about it later. For now, open an author, and modify his birth / death date. You will see that the age field is updated, even though the record was not saved. Nice, our UI starts to feel consistent.

Let's detail what we wrote.

@fields.depends('birth_date', 'death_date')

The @fields.depends decorator is defined by tryton specially for this. The goal is to inform tryton that we are going to require the birth_date and death_date fields in the code that follows. This is more or less the same as the depends attribute in field definitions, but for methods.

Here, tryton will use it to inform the client that calling the on_change_with_age method requires to "send" those two fields to the server.

def on_change_with_age(self):

This works more or less as it did for the default method: when tryton sees a method named on_change_with_<field_name>, it treats it particularly. Defining this method means that, when any one of the fields set in the @fields.depends decorator is modified, the client should call this method on the server to update the <field_name> field.

Basically, remember how we explained that field informations were sent to the client when it required to display a view (It was [when we talked about the Eval keyword][4.4-step6)? In fact the server sends many informations to the client, including this one: what on_change_with_ methods should be called when each field in the view is modified ?

So here, the client knows that when the birth_date or death_date fields are modified it should call the on_change_with method for the age field on the server. The return value of the method will then be used to update the age field client side.

By the way, you may have noticed that the implementation of the method is the same than that of the getter of the age field. How sad to duplicate our code... Fortunately, we can merge both methods. Remove the getter_age method, and update the on_change_with_age prototype as follows:

def on_change_with_age(self, name=None):

Remember to properly update the second parameter of the age field to 'on_change_with_age'.

What we did here is something rather common when working with tryton: if you have a Function field for which the getter is an instancemethod, and for which we want dynamicity (i.e. on -the-fly computation), it is possible to use an on_change_with method as the getter, as long as we add name=None in the parameters, so that the method matches the getter prototype as well as the on_change_with prototype.

Warning: When in an on_change_with method, the only fields which are available on self are those that were defined in the @fields.depends decorator. As a rule of thumb, any field which is accessed through self must be in @fields.depends

As for Function fields, on_change methods are expected to return:

  • An id for Many2One fields
  • A list of ids for Many2Many fields
  • A list of dictionaries for One2Many fields
  • The value for all other types

On Change

on_change_with methods are interesting when you have a single field to manage, but what when one field centralize a lot of modifications? For instance, in the library.author model, the three fields number_of_books, genres and latest_book all depends on the books field, and writing one on_change_with for each of those may be inefficient.

Add the following method on the library.author model:

@fields.depends('books')
def on_change_books(self):
    if not self.books:
        self.genres = []
        self.number_of_books = 0
        return
    self.number_of_books, genres = 0, set()
    for book in self.books:
        self.number_of_books += 1
        if book.genre:
            genres.add(book.genre)
    self.genres = list(genres)

Here the @fields.depends part does not exactly have the same role than for on_change_with methods. We already know what field modification should trigger the method, and that would be books, because that is what a method named on_change_<field> means: when the field is modified, call the server (passing the fields that are in @fields.depends as parameters), and it will update "other" fields.

So here, if we modify the books field client side (for instance by adding a new book), the number_of_books and genres fields will be updated accordingly.

In on_change methods, you can modify any field of the model by just setting its value (self.number_of_books += 1).

You will note that the number_of_books field is not in @fields.depends. That is because we do not need its current value, we are going to override it anyway. Basically, you should consider that calling self.<field_name> on a field that is not in @fields.depends will raise an error. Setting it is okay, but no reading.

WARNING: for One2Many fields, you MUST set the attribute when modifying data inside the list elements. Imagine the case of a main_editor field on library.author, whose modification should be propagated to all books:

@fields.depends('books', 'main_editor')
def on_change_main_editor(self):
    for book in self.books:
        book.editor = self.main_editor

The above code will not work, because tryton only tracks the fields modifications on the main object (here, self). Modifications on "sub-objects" are not tracked, unless explicitely marked:

@fields.depends('books', 'main_editor')
def on_change_main_editor(self):
    for book in self.books:
        book.editor = self.main_editor
    self.books = list(self.books)

This is a frequent cause of errors / broken keyboards, so be careful.

What's the difference?

The difference is:

  • an on_change_with method modifies one field (the one after which the method is named) when any of the @fields.depends fields is modified
  • an on_change method modifies any field when one field (the one named in the method) is modified

Usually, anything you may want to do with one of the two can be done with the other. For instance, we could have manage the on_change_with_age of the previous part with two on_changes on both birth_date and death_date. It is all a question of what is easier to read / the most efficient / what you are the most comfortable with.

Also, remember that you mayh have a free on_change_with for every Function field whose getter is an instance method.

Note: an interesting side-effect is that you should not create fields whose name is prefixed with with_ unless you love debugging

Default on change

There is a particular use case of the on_change method which is more of a default usage.

When the user creates a new book from the library.author view, you can use the on_change_author method to initialize its values depending on the author. Imagine we had a author_name Function field in library.book that we wanted to be dynamically calculated (without having to wait for a save). That means on_change. However, writing self.author.name in an on_change will crash unless the author is already saved. There is one exception though:

@fields.depends('author')
def on_change_author(self):
    self.author_name = self.author.name if self.author else ''

If there is an on_change defined for the field which is the parent (as in required / ondelete='CASCADE' Many2One field) from which the record is being created (i.e. the book is being created from within the author's view), this on_change will be called as soon as a the new instance is displayed to the user, and can so be used to initiliaze some fields depending on the parent values.

Consistency

A typical use case for on_change methods is to ensure consistency in the user experience. For instance, in the application, create a new author. Give it a birth date and death date, then empty the birth date. You cannot save, because the birth_date field is required. This is caused by the states on birth_date, which makes it required when death_date is set. But death_date is invisible since we cleared birth_date. This is a bad user experience that can be fixed with a little on_change:

@fields.depends('birth_date')
def on_change_birth_date(self):
    if not self.birth_date:
        self.death_date = None

Now when we clear birth_date, death_date will be cleared as well, and the problem above disappears.

With states, domains, default and on_change methods, all tryton tools for making a proper user interface and user experience are now available to you.

Homework

You are going to add default values and on_changes:

  • Make sure the description field is always set, by setting it from the summary field if necessary
  • Automatically set the genre field if the editor is set and has only one genre. Also, clear the genre when the editor is changed, and the currently selected genere is not in its list
  • Add a default exemplary when creating a new book. Be consistent.

Once you are done, you can compare your code with that of the step7_homework branch for differences. You can then read here about what you should have done, and why.

What's next

Our user interface and experience is getting better, and we can now make sure there are no inconsistencies between what we require from our users and what they can do.

Next we are going to add a new tool in our UI toolbox, Wizards.

Clone this wiki locally