-
Notifications
You must be signed in to change notification settings - Fork 30
4.4 step7
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!
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 thedefault_
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 thedefault_
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
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_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.
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_change
s 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
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.
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.
You are going to add default values and on_changes:
- Make sure the
description
field is always set, by setting it from thesummary
field if necessary - Automatically set the
genre
field if theeditor
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.
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.