Skip to content

5.0 step5 correction

Jean Cavallo edited this page Jan 18, 2019 · 1 revision

Step 5 - Homework correction

The date at which a book was published

This is a real field on library.book, the information does not exist yet:

publishing_date = fields.Date('Publishing date')

The genres an author has written in

This is a Function field of the Many2Many type on library.author. Calculating this field could be expensive (it requires to iterate over all the books the author wrote), however there is no reason for it to be calculated on many records at once, so it can be an instancemethod. A classmethod would be ideal, though more complicated to write. A searcher could be interesting for searching all authors that wrote in the same genre:

genres = fields.Function(
    fields.Many2Many('library.genre', None, None, 'Genres'),
    'getter_genres', searcher='searcher_genres')

Note that when a Many2Many field is used as the first parameter of a Function field, the parameters are slightly changed. Remember, the standard definition requires the __name__ of an intermediary model, and that of the two fields to use on this model to map the source and destination models. Here, the first parameter is directly the __name__ of the model we want as output for our field (here library.genre), and the second and third parameters are empty.

Note: For One2Many Function fields, the second parameter (referencing the Many2One field to use in the database for materializing the relation) should be set to None as well

Here is an exemple implementation for an instancemethod getter (with an empty searcher):

def getter_genres(self, name):
    genres = set()
    for book in self.books:
        if book.genre:
            genres.add(book.genre.id)
    return list(genres)

@classmethod
def searcher_genres(cls, name, clause):
    return []

Python: set objects are uniquified lists

The getter for a Many2Many Function field must return a list of ids for the target model. The searcher's default value is an empty list, so we will stick to that for now.

The most recent exemplary of a book

This is a Many2One Function field on library.book. Same as above, it will not be used often enough to justify using a classmethod, though it would increase performances. A searcher is probably not necessary here, it is really more of an informative field:

latest_exemplary = fields.Function(
    fields.Many2One('library.book.exemplary', 'Latest exemplary'),
    'getter_latest_exemplary')

def getter_latest_exemplary(self, name):
    latest = None
    for exemplary in self.exemplaries:
        if not exemplary.acquisition_date:
            continue
        if not latest or(
                latest.acquisition_date < exemplary.acquisition_date):
            latest = exemplary
    return latest.id if latest else None

Here, using a classmethod would improve performances, because even though self.exemplaries[0] only tries to access the first ([0]) element of self.exemplaries, it requires tryton to load the exemplaries field, which is all exemplaries, not just the first one. In a classmethod, we could do it all in a query.

Also, we have an example of how Function fields may be complicated: how to handle the case of incomplete data? Here we chose to ignore exemplaries for which the acquisition_date is unknown, but that could depend on your requirements.

Note: We could not juste return latest, because the getter of a Many2One Function field must return an id, not a python object

The ISBN of a book

This is a Char field on library.book, for which we can use the size attribute to limit it to 13 characters. We could have wanted an Integer field here, but:

  • 13 digits is too much for an Integer to hold
  • Adding the constraint, though possible, would have been a little more complicated
isbn = fields.Char('ISBN', size=13,
    help='The International Standard Book Number')

This field should be added in both the form and tree views of the library.book model.

The most recent book an author wrote

A Many2One Function field on library.author. This is an information you may want to see on the list view of an author, so a classmethod may be advisable. A searcher is not necessary though ("I want to know the author whose latest book is XXX" => Better to open the book and see the author for yourself directly):

latest_book = fields.Function(
    fields.Many2One('library.book', 'Latest Book'),
    'getter_latest_book')

@classmethod
def getter_latest_book(cls, authors, name):
    result = {x.id: 0 for x in authors}
    Book = Pool().get('library.book')
    book = Book.__table__()
    sub_book = Book.__table__()
    cursor = Transaction().connection.cursor()

    sub_query = sub_book.select(sub_book.author,
        Max(Coalesce(sub_book.publishing_date, datetime.date.min),
            window=Window([sub_book.author])).as_('max_date'),
        where=sub_book.author.in_([x.id for x in authors]))

    cursor.execute(*book.join(sub_query,
            condition=(book.author == sub_query.author)
            & (Coalesce(book.publishing_date, datetime.date.min)
                == sub_query.max_date)
            ).select(book.author, book.id))
    for author_id, book in cursor.fetchall():
        result[author_id] = book
    return result

The implementation here is complicated, because the actual SQL query is. The basics are identical anyway to the classmethod getter we already wrote, only the query itself is different. If you are familiar enough with SQL, you can compare this implementation with:

SELECT
    "a"."author",
    "a"."id"
FROM
    "library_book" AS "a"
    INNER JOIN
        (SELECT
            "c"."author",
            MAX(COALESCE("c"."publishing_date", '0001-01-01'::date)) OVER "d" AS "max_date"
        FROM "library_book" AS "c"
        WHERE ("c"."author" IN (1, 2, 3, 4))
        WINDOW "d" AS (PARTITION BY "c"."author")
        ) AS "b"
    ON (
        ("a"."author" = "b"."author") AND
        (coalesce("a"."publishing_date", '0001-01-01'::date) = "b"."max_date"))

The number of exemplaries of a book

This Integer Function field on library.book should be fairly easy to implement, since it is very similar to the number_of_books field of library.author. There is no need for a searcher.

The number of books an editor published

Same as above, but on library.editor. Be careful to properly use the editor field of the library.book model in the query to match the editors we are working on.

The list of books an editor published in the past year

This one is similar to the genres field we created on the library.author model, but with an additional constraint on the publishing date. Also, the number of books an editor publishes is vastly superior to that of a single author, so iterating on them in an instancemethod may be a performance killer. So it should be a classmethod.

We will not implement it for now, it is possible to get this information another way that we will talk about later.