-
Notifications
You must be signed in to change notification settings - Fork 31
4.4 step5
We now have a model, data and the possibility to use them. Once
again, you should update your environment on the step5
branch to make sure
that you are up to date:
git checkout 4.4/step5
This step fill be fully dedicated to Function
fields. What makes them so
important ?
What you need to understand is that tryton uses fields for everything:
- You cannot display anything but fields to the end user
- You need fields to add constraints (more on this later)
- You also need fields to fine-tune the interface
So let's focus on the first point, which is easier to understand. We told earlier that you should as much as possible avoid to duplicate information. We even said that, for instance, storing the number of books of an author should not be done with a field, because it could be calculated.
However, you cannot display an information that is not a field. So showing the user how many books an author wrote will require a field anyway, even though we know we can calculate it from existing data.
Enter Function
fields. As you may have guessed by now, Function
fields
are calculated fields. They allow to:
- Avoid data duplication
- Display any kind of information. It can be as simple as a "shortcut" (for instance if we want to display the author's birth date directly on a book), to complex calculated informations (how many books of the same genre the author had already written when the book was released), or even external informations fetched from another system or the internet (we could imagine getting a global popularity ranking for our book)
- Be tryton fields, which allows for a lot of things as we will detail later
Open the library.py
file and add the following after the gender
field
of the library.author
model:
age = fields.Function(
fields.Integer('Age'),
'getter_age')
This is the most basic Function
field that you will write, and encounter.
Function
fields are declared the same way as other fields, the difference
is in the parameters.
The first parameters is another field instance. This instance will contains
all the standard information of your field. Its type obviously (here, it will
be an Integer
), all the parameters we already saw (help
,
required
, etc.) and those we did not (yet). The easy way (at first) to
write your function field is to write it the same way you would if it was a
real field, and then add it as the fields.Function
first parameter.
The second parameter describes how the field's value will be computed. The expected value of this parameter is the name of a method on the model. This method will be called to compute the field's value for the required records.
Note: The logic of using a string as the name of a method that must be called under certain circumstances is rather frequent in tryton, this is just the first time we are seeing it
There are multiple ways to write the getter_age
method, we are going to go
with the easy one this time. First, at the top of your file, import the python
datetime
module:
import datetime
Now, in the body of the Author
class, under the field declarations, add:
def getter_age(self, name):
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
We will not talk about the algorithm, which is not the purpose of this training
module. However, we will explain how this will calculate the age
field
value.
The getter_age
method's name matches the second parameter of the age
field definition. So when tryton will need to read
the age
of a
library.author
record, he will look for this method. It is an instance
method,
so the first parameter (self
) will contain the record that we want to
calculate the age for.
Warning: it is important to understand that a "record" of the
library.author
model is not the same as an instance of the Author
class.
The "record" is a python object created by the python server from a class which
inherit from the Author
class, but which may contain many other things as
well
The second parameter contains the name of the Function
field for which the
method was called. It is used when the same getter method is used for different
but similar Function
fields which are calculated the same way. In that case
the name
parameter allows to slightly tweak the algorithm accordingly.
The expected return value of the method is the value we want to read in the
age
field. The returned type depends on the type of the first parameter of
the Function
field:
- For basic data fields (
Char
,Text
,Integer
, etc...), the associated python type - For
Numeric
, instances of theDecimal
class - For
Many2One
fields, theid
of the record we want to reference - For
Many2Many
, a list ofid
s - For
One2Many
, a list of dictionaries, with the key / values of the target records we need, or a list ofid
s
Warning: The id
of a record is not the same thing as the id
attribute we already saw in XML files. A record's id
is a technical integer
field that is managed by tryton (set once and for all when creating the
record), and which uses as a foreign key in the database to manage relations
between models
Additionally, all Function
fields getters may return None
as a value
for the field.
Regarding the actual code of the getter, you can see that we can access the
values of the birth_date
or death_date
fields for the current record by
using self.birth_date
or self.death_date
. This is possible in all the
server code, and tryton will do what it takes to get the information. In the
birth_date
case, for instance, it will:
- Check that the current used is allowed to read the field
- If the field value is already in memory, return it
- If not, it will load it from the database (if
birth_date
was aFunction
field, this is where thegetter
would be called) - Then store it in a cache, and return it
Now that we got our field, we can display it like any other field. Add it in
the form view of the library.author
model, after the death_date
field.
Reload the server, the view, and the age field should appear.
If you do not already have, create a new author, and set it a birth_date
and death_date
. You will notice that the age stays empty until you hit the
"Save" button. Also, if you modify any of the dates, the age is not recomputed
until you save the record. This is because Function
fields are only
calculated when records are read
.
Reading a record happens when:
- You try to access it "server-side" by writing
my_record.age
- The record is displayed client-side
- The record is saved client-side (saving always triggers a
read
right after) - You manually call the
read
method on your record
There are means to make the age be refreshed in real time, which we will talk about later.
Add another field on the library.author
model:
number_of_books = fields.Function(
fields.Integer('Number of books'),
'getter_number_of_books')
Finally, the information we wanted to have but could not because it would be a
"duplicate"... but why under a Performances title? The reason is that the
number of books
information requires to either iterate over the list of
books of the author, or directly query the database.
Enters tryton performance optimisations. When the client displays 100 records
to the user (for instance, 100 authors) in a list, it will make one
read
call to the server for the 100 records at once! This optimize the
bandwidth usage between the server and the client.
On the server side, if the server is asked to read 100 records at once, it will
try to make only one database call for the same reasons (and also, the overhead
of a database query is very big compared with "simple" read queries).
All this leads to: with the getter
definition we saw above, reading 100
records would trigger 100 calls to it. In the case of the number_of_books
field, the basic implementation would be very inefficient, because we would
have to do another 100 database calls to get the value of the field.
Fortunately, tryton provides a way to do this in an elegant fashion. Add those
imports at the top of your file:
from sql.aggregate import Count
from trytond.pool import Pool
from trytond.transaction import Transaction
and then the getter implementation (under the existing getter_age
method):
@classmethod
def getter_number_of_books(cls, authors, name):
result = {x.id: 0 for x in authors}
Book = Pool().get('library.book')
book = Book.__table__()
cursor = Transaction().connection.cursor()
cursor.execute(*book.select(book.author, Count(book.id),
where=book.author.in_([x.id for x in authors]),
group_by=[book.author]))
for author_id, count in cursor.fetchall():
result[author_id] = count
return result
Let's detail all of this.
The obvious difference between getter_age
and getter_number_of_books
is
that they do not even have the same prototype. The latter is a classmethod, and
it has cls
and authors
as parameters in place of the self
of the
former. How do we go from one to the other ?
Well, remember the scenario with 100 records being read at once ? In this case,
the getter_number_of_books
would be called once, with the 100 records
passed as the second argument, authors
. The expected result in this
case is a dictionary, with the record's id
as keys, and the result (number
of books for the author) as values.
Note: Since python parameters are not typed, it is important that parameter
names are properly named. Here, authors
is author
from
library.author
and s
, for a list of authors
Now the implementation. We could, as we talked about earlier, iterate on all
authors in the list, then query the database for its number of book. However,
in that case we could as well use the instance method kind of getter we used
for the age
field, there will still be 100 requests sent to the database.
What we are doing here is doing only one query to get the number of books
for all 100 records at once. To do so, we use the
sql
module, which is internally
used by tryton for all database related operations. We will now go through the
code, line by line, and explain what is going on.
result = {x.id: 0 for x in authors}
We must initialize the return value for all authors. We are going to query the
database on the library_book
table, grouping by authors. However, if an
author does not have any book, we must still return a value for the
number_of_books
field, so we take care of this by initializing the return
value to 0
for all authors.
As explained earlier, we are going to query the library_book
table. To be
more accurate, we are going to query the table which tryton uses to store the
data about records of the library.book
model. Usually, the name of the
table created by tryton for a model will be derived from the model's
__name__
(again) by replacing the dots (.
) with underscores (_
).
However, this may not always be the case, depending on various factors. So we
will use the proper way to get a database table for a query: by asking tryton
to give us one:
Book = Pool().get('library.book')
book = Book.__table__()
You will see (and write) the first line or variants of it very often when
developing tryton modules. The Book
variable (note that it is capitalized)
is going to hold the model class for the library.book
model. This model
IS NOT the Book
class later defined in the library.py
file. In a few
words, it is a class that is build by tryton to include the Book
class,
and all modifications that are made by other installed modules to the
library.book
model. If we imagine another module that adds a new field on
the library.book
model, the Book
variable that we got here will contain
the information about this field, when the Book
class of the library.py
file does not.
Long story short, when you need to get the class that corresponds to a model,
you should use Pool().get('<model_name>')
.
Note: The Pool
we got here is the same one than that of the
__init__.py
file, in which we registerd our python classes
So our Book
variable now holds the python class that tryton uses to
represent the library.book
model. We then call the __table__
method on
it to get a sql table object for the library.book
model.
Note: You could have directly written:
from sql import Table
book = Table('library_book')
however there would still be the risk that you be in one of the few case where
the database table name is not directly derived from the __name__
of the
model
We got a table object, let's go on:
cursor = Transaction().connection.cursor()
The Transaction
object will be covered in depth later on. Here we use it to
get an open connection to the database, which is the only way to directly
execute queries. That is how tryton does internally when it needs to
manipulate the database, and something that you will have to do, more or less
often depending on your needs.
The rest of the code should be straightforward. We execute our query with the
cursor that the Transaction
gave us, parse the return values, update the
result, then return it.
We successfully managed to group the computation of our number_of_books
field, so that reading 100 authors at once will only make one query (for this
field).
The rules to name a Function
field are the same than those of "standard"
fields. They are usually located after the "normal" fields, but there may be
exceptions for particularly important Function
fields.
The getter
of a Function
fields should start with getter_
or less
ideally get_
. The latter is prefered by some, though it does not directly
identify the method as a getter
because there are many non-getter methods
which start with get_
.
Finally, the second parameter of the classmethod Function
fields should be
named so as to reflect their contents. This is not a rule specific to getters,
but this is the first time we got to talk about it. We do not know the
parameters type from just reading the method declaration, so that got to be
included in the variables names when possible.
There are no absolutes, but guidelines:
- If the instance method contains database queries / searches, or iterations on non trivial lists, going classmethod may significatively improve performances
- A field displayed in a tree / list view will always be "mass-read" by tryton. So if its getter has bad performances, it will be directly noticeable by the end-user. It is a strong case for classmethod getters
However:
- An expensive
Function
field that will be called once in a while, on few records, may not be worth rewriting in a classmethod - All instancemethod getters cannot be optimized by being converted to classmethods
- Classmethod getters code is usually more complicated to understand. A small performance increase at the cost of unreadable code may not be worth
- "premature optimization is the root of all evil"
There is a rarely used version of the classmethod getter which uses the
names
parameter rather than name
. Tryton is able to detect this and
behaves differently in this case. However, the use cases are very limited, so
we will not cover this case here.
Function
fields have an optional third argument, the setter
. When set,
it will make the field modifiable. The setter
value must be, as the
getter
, the name of a function that will be called to set the field
value. Its prototype is the following:
@classmethod
def setter_my_field_name(cls, instances, name, value):
pass
Here, the instances
parameter will contain the list of records to update,
name
the name of the field (the same than for the getter
function), and
value
the value that should be set for the field.
We will not use setters in this training module, because it has some inconvenients that can be avoided in most cases by other means (that we will talk about later):
- When a user sets multiple
Function
fields with setters, then saves, the setters are called in an unknown order, which may be inconvenient to manage - Setters are called after the other fields are set, and will trigger additional validation calls after they made their modifications
All in all, they exist but are not often used. You way consider using them in some cases, but it should be limited to specific needs.
The second, more often used, optional parameter of Function
fields is the
searcher
. When a user tries to apply filters on a model (for instance, he
wants to find all authors who were born before a given date), tryton converts
the search parameters to a SQL query which is then executed on the database.
This works well for "hard" data fields, which are in fact columns in the
database (birth_date < XXXX
=> WHERE author.birth_date < 'XXXX'
), but
not so well for Function
fields. Indeed, tryton has no way to guess how a
age > XXXX
can be converted to a database query, since the age
column
does not exist.
Searchers provides a way to explain to tryton how to convert the age > XXXX
query to a database query. The code is usually rather similar to that of the
classmethod getters, so writing a searcher
may be a good occasion to
upgrade an instancemethod getter to a more efficient classmethod.
Python: searcher
is an optional argument, so when setting one, you should
write it like so:
my_field = fields.Function(
fields.Char('My Field'),
'getter_my_field', searcher='searcher_my_field')
Generally speaking, when a parameter is optional, you should always specify it when calling the method / class, even when it is not stricly necessary
Tryton will not let you search on Function
fields for which no searcher
is defined (meaning it will raise an error), because it really does not know
how to perform the search. Searchers are usually not defined for all
Function
fields, because:
- There are many small fields that the user has no reason to search on
- Writing a
searcher
requires better understanding of both the model, and SQL, so it may take some development and maintenance time
There are some use cases right now that we could implement in our library module, but that will come after we had a talk on how searches are technically done in tryton, in the next step.
As explained in step 4, all columns that are displayed to the
users in a tree view can be clicked on to order the list of values on that
column. The same way as for searches, tryton converts the request to order in a
SQL query which includes the order criterion. The same as searches,
Function
fields cannot be natively ordered (the tryton client usually
raises an error when trying to do so).
It is possible to make it possible for tryton to order a Function
fields.
Contrary to getter
, setter
or searcher
methods, it is not done
through an argument in the field declaration, but simply by declaring a
classmethod named def order_<my_field_name>
.
The implementation of this method is outside the scope of this module, consult the tryton documentation for more details.
Function
fields are essential to writing good tryton modules. They allow to
avoid data duplication across modules, display calculated informations to the
user, and is unavoidable when trying to fine tune the UI (more on this on the
next step).
Properly deciding between instancemethods and classmethods is important, since it may cause performances problems. Sometimes the answer is obvious, sometimes it requires some consideration since the optimization may be too costly, and the field not so often used.
We saw in the previous step that we could use the _rec_name
variable to tell to tryton how it should display a record. For now, we set it
to existing Char
fields, and it looked good.
There is a hidden Function
field in tryton, named rec_name
. This mean
that if there is no existing field that does what you need, you can use it to
write the string you want.
Add the following code in library.book.exemplary
:
def get_rec_name(self, name):
return '%s: %s' % (self.book.rec_name, self.identifier)
And voilà. Our exemplaries will now be identified by both the name of the book they are linked to, as well as their unique identifier, which will be far more useful.
You can also replace the identifier
field in the
view/exemplary_list.xml
file so that it uses rec_name
.
You will have to add the following fields in your module. For each of them, ask yourself the following questions:
- Should it be a
Function
field ? - If so, should the getter be an instancemethod or classmethod ?
- Should there be a searcher method ?
For every field you should follow the rules that were detailed in step
3 regarding naming, positioning, etc. You should as well add the
fields in the views, and check that they are properly calculated. If there are
Function
fields for which you think there should be a searcher, declare it
but leave it empty for now (we did not learn how to write it yet).
Here are the fields that you should add:
- The date at which a book was published
- The genres an author has written in
- The most recent exemplary of a book
- The ISBN of a book
- The most recent book an author wrote
- The number of exemplaries of a book
- The number of books an editor published
- The list of books an editor published in the past year
Once you are done, you can compare your code with that of the
step5_homework
branch for differences. You can then read
here about what you should have done, and why.
We are fleshing up our model with calculated informations, which are the last building blocks of tryton.
Next we are going to add constraints and rules on our models / fields to have consistency across our data.