diff --git a/src/base/tests/test_util.py b/src/base/tests/test_util.py index 325368fe1..30fb830ba 100644 --- a/src/base/tests/test_util.py +++ b/src/base/tests/test_util.py @@ -2677,3 +2677,63 @@ def test_assert_updated_combo(self): ): p2.city = "Niflheim" util.flush(p2) + + +class TestReportUtils(UnitTestCase): + @parametrize( + [ + ( + "Simple test with minimal arguments and no data.", + [], + ("id", "name"), + "Partner {name} has id {id}", + None, + None, + 100, + "Other", + "Simple test with minimal arguments and no data.", + ), + ( + "Testing links.", + [], + ("id", "name"), + "Partner {partner_link}.", + {"partner_link": ("res.partner", "id", "name")}, + None, + 100, + "Other", + "Testing links.", + ), + ( + "Test with minimal data.", + [(1, "Partner One")], + ("id", "name"), + "Partner {partner_link}.", + {"partner_link": ("res.partner", "id", "name")}, + None, + 100, + "Other", + "Test with minimal data.
The total number of affected records is 1.
", + ), + ( + "Test with limited data.", + [(1, "Partner One"), (2, "Partner Two"), (3, "Partner Three")], + ("id", "name"), + "Partner {partner_link}.", + {"partner_link": ("res.partner", "id", "name")}, + None, + 2, + "Other", + "Test with limited data.
The total number of affected records is 3. This list is limited to 2 records.
", + ), + ] + ) + def test_report_with_list(self, summary, data, columns, row_format, links, total, limit, category, expected): + self.assertEqual( + util.report_with_list(summary, data, columns, row_format, links, total, limit, category), expected + ) diff --git a/src/util/report.py b/src/util/report.py index 2dd965aba..c0968545b 100644 --- a/src/util/report.py +++ b/src/util/report.py @@ -106,7 +106,7 @@ def html_escape(text): } -def add_to_migration_reports(message, category="Other", format="text"): +def report(message, category="Other", format="text"): assert format in {"text", "html", "md", "rst"} if format == "md": message = md2html(dedent(message)) @@ -126,6 +126,89 @@ 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) +add_to_migration_reports = report + + +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 = ( + "{}
{}
".format(summary, details) + if details + else "{}".format(summary) + ) + report(message=msg, category=category, format="html") + return msg + + +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 SQL query, or any list. + + .. example:: + + .. code-block:: python + + 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" + ) + + :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: columns in `data`, can be referenced in `row_format`. + :param str row_format: format for rows, can use any name from `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, + the keys can be referenced 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)) + if links: + 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 "
  • {}
  • ".format(row_format.format(**row_dict)) + + if not data: + row_to_html(columns) # Validate the format is correct, including links + return report_with_summary(summary=summary, details="", category=category) + + 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 = "" + return report_with_summary(summary, "{}{}".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: