Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions src/util/report.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,79 @@ def add_to_migration_reports(message, category="Other", format="text"):
_logger.warning("Upgrade report is growing suspiciously long: %s characters so far.", migration_reports_length)


report = add_to_migration_reports


def report_with_summary(summary, details, category="Other"):
"""Append the upgrade report with a new entry.

:param str summary: Description of a report entry.
:param str details: Detailed description that is going to be folded by default.
:param str category: Title of a report entry.
"""
msg = (
"<summary>{}<details>{}</details></summary>".format(summary, details)
if details
else "<summary>{}</summary>".format(summary)
)
report(message=msg, category=category, format="html")


def report_with_list(summary, data, columns, row_format, links=None, total=None, limit=100, category="Other"):
"""Append the upgrade report with a new entry that displays a list of records.

The entry consists of a category (title) and a summary (body).
The entry displays a list of records previously returned by an SQL query, or any list as long as it's passed as a Python List of Tuples.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Try to keep the lines at max 90 chars (similar as we do for other docstrings here)

Suggested change
The entry displays a list of records previously returned by an SQL query, or any list as long as it's passed as a Python List of Tuples.
The entry displays a list of records previously returned by an SQL query, or any list
as long as it's passed as a Python List of Tuples.


.. example::

.. code-block:: python

util.report_with_list(summary="The following records were altered.",
data=cr.fetchall(),
columns=("id", "name", "city", "comment", "company_id", "company_name")
row_format="Partner with id {partner_link} works at company {company_link} in {city}, ({comment})",
links={"company_link": ("res.company", "company_id", "company_name"), "partner_link": ("res.partner", "id", "name")},
category="Accounting"
)
Comment on lines +157 to +163
Copy link
Contributor

@aj-fuentes aj-fuentes Dec 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The usual formatting allows for shorter lines. Unless there is some rendering issue?

Suggested change
util.report_with_list(summary="The following records were altered.",
data=cr.fetchall(),
columns=("id", "name", "city", "comment", "company_id", "company_name")
row_format="Partner with id {partner_link} works at company {company_link} in {city}, ({comment})",
links={"company_link": ("res.company", "company_id", "company_name"), "partner_link": ("res.partner", "id", "name")},
category="Accounting"
)
total = cr.rowcount
data = cr.fetchmany(20)
util.report_with_list(
summary="The following records were altered.",
data=data,
columns=("id", "name", "city", "comment", "company_id", "company_name")
row_format="Partner with id {partner_link} works at company {company_link} in {city}, ({comment})",
links={
"company_link": ("res.company", "company_id", "company_name"),
"partner_link": ("res.partner", "id", "name")
},
total=total,
category="Accounting"
)

EDIT: we can include as well in the example total=...


:param str summary: description of a report entry.
:param list(tuple) data: data to report, each entry would be a row in the report.
It could be empty, in which case only the summary is rendered.
:param tuple(str) columns: names for each column in "data", can be referenced in "row_format".
:param str row_format: the way a row in a list is formatted, using named placeholders, e.g.:
"Partner {partner_link} that lives in {city} works at company {company_link}."
:param dict(str, tuple(str, str, str)) links: optional model/record links spec,
the keys can be referenced in "row_format".
:param int total: if the original list was limited prior to calling this method, the original, total number of
records, can be provided with this parameter.
:param int limit: the maximum number of records that are going to be displayed in the report.
Comment on lines +166 to +175
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some ideas to shorten this and make to the point.
Also use back-quote instead of double quotes to refer to code.

Suggested change
:param list(tuple) data: data to report, each entry would be a row in the report.
It could be empty, in which case only the summary is rendered.
:param tuple(str) columns: names for each column in "data", can be referenced in "row_format".
:param str row_format: the way a row in a list is formatted, using named placeholders, e.g.:
"Partner {partner_link} that lives in {city} works at company {company_link}."
:param dict(str, tuple(str, str, str)) links: optional model/record links spec,
the keys can be referenced in "row_format".
:param int total: if the original list was limited prior to calling this method, the original, total number of
records, can be provided with this parameter.
:param int limit: the maximum number of records that are going to be displayed in the report.
:param list(tuple) data: data to report, each entry would be a row in the report.
It could be empty, in which case only the summary is rendered.
:param tuple(str) columns: columns in `data`, can be referenced in `row_format`.
:param str row_format: format for rows, can use any name in `columns` or `links`, e.g.:
`"Partner {partner_link} that lives in {city} works at
company {company_link}."`
:param dict(str, tuple(str, str, str)) links: optional model/record links spec,
to use in `row_format`.
:param int total: optional total number of records.
Taken as `len(data)` when `None` is passed.
Useful when `data` was limited by the caller.
:param int limit: maximum number of records to list in the report.
If `data` contains more records the `total` number would be
included in the report as well.

:param str category: title of a report entry.
"""

def row_to_html(row):
row_dict = dict(zip(columns, row))
row_dict.update(
{
link: get_anchor_link_to_record(rec_model, row_dict[id_col], row_dict[name_col])
for link, (rec_model, id_col, name_col) in links.items()
}
)
return "<li>{}</li>".format(row_format.format(**row_dict))

if not data:
row_to_html(columns) # Validate the format is correct, including links
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It will fail. column doesn't have the expected type.

(a.k.a tests are missing)

return report_with_summary(summary=summary, details="", category=category)

total = total or len(data)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To avoid weird situation where the number of entries in data is lower than the limit, but we still got total, we should adapt the limit
Do not use the old-style or shortcut, we can be explicit here: None is a special value.

Suggested change
total = total or len(data)
limit = min(limit, len(data))
total = len(data) if total is None else total

disclaimer = "The total number of affected records is {}.".format(total)
if total > limit:
disclaimer += " This list is limited to {} records.".format(limit)

rows = "<ul>\n" + "\n".join([row_to_html(row) for row in data[:limit]]) + "\n</ul>"
return report_with_summary(summary, "<i>{}</i>{}".format(disclaimer, rows), category)


def announce_release_note(cr):
filepath = os.path.join(os.path.dirname(__file__), "release-note.xml")
with open(filepath, "rb") as fp:
Expand Down