Ejemplo n.º 1
0
        async def extra_template():
            display_rows = []
            for row in results.rows:
                display_row = []
                for column, value in zip(results.columns, row):
                    display_value = value
                    # Let the plugins have a go
                    # pylint: disable=no-member
                    plugin_value = pm.hook.render_cell(
                        value=value,
                        column=column,
                        table=None,
                        database=database,
                        datasette=self.ds,
                    )
                    if plugin_value is not None:
                        display_value = plugin_value
                    else:
                        if value in ("", None):
                            display_value = jinja2.Markup(" ")
                        elif is_url(str(display_value).strip()):
                            display_value = jinja2.Markup(
                                '<a href="{url}">{url}</a>'.format(
                                    url=jinja2.escape(value.strip())))
                    display_row.append(display_value)
                display_rows.append(display_row)

            # Show 'Edit SQL' button only if:
            # - User is allowed to execute SQL
            # - SQL is an approved SELECT statement
            # - No magic parameters, so no :_ in the SQL string
            edit_sql_url = None
            is_validated_sql = False
            try:
                validate_sql_select(sql)
                is_validated_sql = True
            except InvalidSql:
                pass
            if allow_execute_sql and is_validated_sql and ":_" not in sql:
                edit_sql_url = (self.ds.urls.database(database) + "?" +
                                urlencode({
                                    **{
                                        "sql": sql,
                                    },
                                    **named_parameter_values,
                                }))
            return {
                "display_rows": display_rows,
                "custom_sql": True,
                "named_parameter_values": named_parameter_values,
                "editable": editable,
                "canned_query": canned_query,
                "edit_sql_url": edit_sql_url,
                "metadata": metadata,
                "config": self.ds.config_dict(),
                "request": request,
                "path_with_added_args": path_with_added_args,
                "path_with_removed_args": path_with_removed_args,
                "hide_sql": "_hide_sql" in params,
            }
Ejemplo n.º 2
0
 async def extra_template():
     display_rows = []
     for row in results.rows:
         display_row = []
         for value in row:
             display_value = value
             # Let the plugins have a go
             plugin_value = pm.hook.render_cell(value=value)
             if plugin_value is not None:
                 display_value = plugin_value
             else:
                 if value in ("", None):
                     display_value = jinja2.Markup("&nbsp;")
                 elif is_url(str(display_value).strip()):
                     display_value = jinja2.Markup(
                         '<a href="{url}">{url}</a>'.format(
                             url=jinja2.escape(value.strip())))
             display_row.append(display_value)
         display_rows.append(display_row)
     return {
         "display_rows": display_rows,
         "database_hash": hash,
         "custom_sql": True,
         "named_parameter_values": named_parameter_values,
         "editable": editable,
         "canned_query": canned_query,
         "metadata": metadata,
         "config": self.ds.config_dict(),
     }
Ejemplo n.º 3
0
 async def extra_template():
     display_rows = []
     for row in results.rows:
         display_row = []
         for column, value in zip(results.columns, row):
             display_value = value
             # Let the plugins have a go
             # pylint: disable=no-member
             plugin_value = pm.hook.render_cell(
                 value=value,
                 column=column,
                 table=None,
                 database=database,
                 datasette=self.ds,
             )
             if plugin_value is not None:
                 display_value = plugin_value
             else:
                 if value in ("", None):
                     display_value = jinja2.Markup("&nbsp;")
                 elif is_url(str(display_value).strip()):
                     display_value = jinja2.Markup(
                         '<a href="{url}">{url}</a>'.format(
                             url=jinja2.escape(value.strip())
                         )
                     )
             display_row.append(display_value)
         display_rows.append(display_row)
     return {
         "display_rows": display_rows,
         "custom_sql": True,
         "named_parameter_values": named_parameter_values,
         "editable": editable,
         "canned_query": canned_query,
         "metadata": metadata,
         "config": self.ds.config_dict(),
         "request": request,
         "path_with_added_args": path_with_added_args,
         "path_with_removed_args": path_with_removed_args,
         "hide_sql": "_hide_sql" in params,
     }
Ejemplo n.º 4
0
def test_is_url(url, expected):
    assert expected == utils.is_url(url)
Ejemplo n.º 5
0
    async def display_columns_and_rows(
        self, database, table, description, rows, link_column=False, truncate_cells=0
    ):
        "Returns columns, rows for specified table - including fancy foreign key treatment"
        db = self.ds.databases[database]
        table_metadata = self.ds.table_metadata(database, table)
        sortable_columns = await self.sortable_columns_for_table(database, table, True)
        columns = [
            {"name": r[0], "sortable": r[0] in sortable_columns} for r in description
        ]
        pks = await db.primary_keys(table)
        column_to_foreign_key_table = {
            fk["column"]: fk["other_table"]
            for fk in await db.foreign_keys_for_table(table)
        }

        cell_rows = []
        base_url = self.ds.config("base_url")
        for row in rows:
            cells = []
            # Unless we are a view, the first column is a link - either to the rowid
            # or to the simple or compound primary key
            if link_column:
                is_special_link_column = len(pks) != 1
                pk_path = path_from_row_pks(row, pks, not pks, False)
                cells.append(
                    {
                        "column": pks[0] if len(pks) == 1 else "Link",
                        "value_type": "pk",
                        "is_special_link_column": is_special_link_column,
                        "raw": pk_path,
                        "value": jinja2.Markup(
                            '<a href="{base_url}{database}/{table}/{flat_pks_quoted}">{flat_pks}</a>'.format(
                                base_url=base_url,
                                database=database,
                                table=urllib.parse.quote_plus(table),
                                flat_pks=str(jinja2.escape(pk_path)),
                                flat_pks_quoted=path_from_row_pks(row, pks, not pks),
                            )
                        ),
                    }
                )

            for value, column_dict in zip(row, columns):
                column = column_dict["name"]
                if link_column and len(pks) == 1 and column == pks[0]:
                    # If there's a simple primary key, don't repeat the value as it's
                    # already shown in the link column.
                    continue

                # First let the plugins have a go
                # pylint: disable=no-member
                plugin_display_value = pm.hook.render_cell(
                    value=value,
                    column=column,
                    table=table,
                    database=database,
                    datasette=self.ds,
                )
                if plugin_display_value is not None:
                    display_value = plugin_display_value
                elif isinstance(value, bytes):
                    display_value = jinja2.Markup(
                        "&lt;Binary&nbsp;data:&nbsp;{}&nbsp;byte{}&gt;".format(
                            len(value), "" if len(value) == 1 else "s"
                        )
                    )
                elif isinstance(value, dict):
                    # It's an expanded foreign key - display link to other row
                    label = value["label"]
                    value = value["value"]
                    # The table we link to depends on the column
                    other_table = column_to_foreign_key_table[column]
                    link_template = (
                        LINK_WITH_LABEL if (label != value) else LINK_WITH_VALUE
                    )
                    display_value = jinja2.Markup(
                        link_template.format(
                            database=database,
                            base_url=base_url,
                            table=urllib.parse.quote_plus(other_table),
                            link_id=urllib.parse.quote_plus(str(value)),
                            id=str(jinja2.escape(value)),
                            label=str(jinja2.escape(label)),
                        )
                    )
                elif value in ("", None):
                    display_value = jinja2.Markup("&nbsp;")
                elif is_url(str(value).strip()):
                    display_value = jinja2.Markup(
                        '<a href="{url}">{url}</a>'.format(
                            url=jinja2.escape(value.strip())
                        )
                    )
                elif column in table_metadata.get("units", {}) and value != "":
                    # Interpret units using pint
                    value = value * ureg(table_metadata["units"][column])
                    # Pint uses floating point which sometimes introduces errors in the compact
                    # representation, which we have to round off to avoid ugliness. In the vast
                    # majority of cases this rounding will be inconsequential. I hope.
                    value = round(value.to_compact(), 6)
                    display_value = jinja2.Markup(
                        "{:~P}".format(value).replace(" ", "&nbsp;")
                    )
                else:
                    display_value = str(value)
                    if truncate_cells and len(display_value) > truncate_cells:
                        display_value = display_value[:truncate_cells] + u"\u2026"

                cells.append(
                    {
                        "column": column,
                        "value": display_value,
                        "raw": value,
                        "value_type": "none"
                        if value is None
                        else str(type(value).__name__),
                    }
                )
            cell_rows.append(Row(cells))

        if link_column:
            # Add the link column header.
            # If it's a simple primary key, we have to remove and re-add that column name at
            # the beginning of the header row.
            if len(pks) == 1:
                columns = [col for col in columns if col["name"] != pks[0]]

            columns = [
                {"name": pks[0] if len(pks) == 1 else "Link", "sortable": len(pks) == 1}
            ] + columns
        return columns, cell_rows
Ejemplo n.º 6
0
        async def extra_template():
            display_rows = []
            for row in results.rows if results else []:
                display_row = []
                for column, value in zip(results.columns, row):
                    display_value = value
                    # Let the plugins have a go
                    # pylint: disable=no-member
                    plugin_display_value = None
                    for candidate in pm.hook.render_cell(
                            value=value,
                            column=column,
                            table=None,
                            database=database,
                            datasette=self.ds,
                    ):
                        candidate = await await_me_maybe(candidate)
                        if candidate is not None:
                            plugin_display_value = candidate
                            break
                    if plugin_display_value is not None:
                        display_value = plugin_display_value
                    else:
                        if value in ("", None):
                            display_value = Markup("&nbsp;")
                        elif is_url(str(display_value).strip()):
                            display_value = Markup(
                                '<a href="{url}">{url}</a>'.format(
                                    url=escape(value.strip())))
                        elif isinstance(display_value, bytes):
                            blob_url = path_with_format(
                                request=request,
                                format="blob",
                                extra_qs={
                                    "_blob_column":
                                    column,
                                    "_blob_hash":
                                    hashlib.sha256(display_value).hexdigest(),
                                },
                            )
                            display_value = Markup(
                                '<a class="blob-download" href="{}">&lt;Binary:&nbsp;{}&nbsp;byte{}&gt;</a>'
                                .format(
                                    blob_url,
                                    len(display_value),
                                    "" if len(value) == 1 else "s",
                                ))
                    display_row.append(display_value)
                display_rows.append(display_row)

            # Show 'Edit SQL' button only if:
            # - User is allowed to execute SQL
            # - SQL is an approved SELECT statement
            # - No magic parameters, so no :_ in the SQL string
            edit_sql_url = None
            is_validated_sql = False
            try:
                validate_sql_select(sql)
                is_validated_sql = True
            except InvalidSql:
                pass
            if allow_execute_sql and is_validated_sql and ":_" not in sql:
                edit_sql_url = (self.ds.urls.database(database) + "?" +
                                urlencode({
                                    **{
                                        "sql": sql,
                                    },
                                    **named_parameter_values,
                                }))

            show_hide_hidden = ""
            if metadata.get("hide_sql"):
                if bool(params.get("_show_sql")):
                    show_hide_link = path_with_removed_args(
                        request, {"_show_sql"})
                    show_hide_text = "hide"
                    show_hide_hidden = (
                        '<input type="hidden" name="_show_sql" value="1">')
                else:
                    show_hide_link = path_with_added_args(
                        request, {"_show_sql": 1})
                    show_hide_text = "show"
            else:
                if bool(params.get("_hide_sql")):
                    show_hide_link = path_with_removed_args(
                        request, {"_hide_sql"})
                    show_hide_text = "show"
                    show_hide_hidden = (
                        '<input type="hidden" name="_hide_sql" value="1">')
                else:
                    show_hide_link = path_with_added_args(
                        request, {"_hide_sql": 1})
                    show_hide_text = "hide"
            hide_sql = show_hide_text == "show"
            return {
                "display_rows": display_rows,
                "custom_sql": True,
                "named_parameter_values": named_parameter_values,
                "editable": editable,
                "canned_query": canned_query,
                "edit_sql_url": edit_sql_url,
                "metadata": metadata,
                "settings": self.ds.settings_dict(),
                "request": request,
                "show_hide_link": show_hide_link,
                "show_hide_text": show_hide_text,
                "show_hide_hidden": markupsafe.Markup(show_hide_hidden),
                "hide_sql": hide_sql,
            }
Ejemplo n.º 7
0
async def display_columns_and_rows(
    datasette,
    database_name,
    table_name,
    description,
    rows,
    link_column=False,
    truncate_cells=0,
    sortable_columns=None,
):
    """Returns columns, rows for specified table - including fancy foreign key treatment"""
    sortable_columns = sortable_columns or set()
    db = datasette.databases[database_name]
    table_metadata = datasette.table_metadata(database_name, table_name)
    column_descriptions = table_metadata.get("columns") or {}
    column_details = {
        col.name: col
        for col in await db.table_column_details(table_name)
    }
    pks = await db.primary_keys(table_name)
    pks_for_display = pks
    if not pks_for_display:
        pks_for_display = ["rowid"]

    columns = []
    for r in description:
        if r[0] == "rowid" and "rowid" not in column_details:
            type_ = "integer"
            notnull = 0
        else:
            type_ = column_details[r[0]].type
            notnull = column_details[r[0]].notnull
        columns.append({
            "name": r[0],
            "sortable": r[0] in sortable_columns,
            "is_pk": r[0] in pks_for_display,
            "type": type_,
            "notnull": notnull,
            "description": column_descriptions.get(r[0]),
        })

    column_to_foreign_key_table = {
        fk["column"]: fk["other_table"]
        for fk in await db.foreign_keys_for_table(table_name)
    }

    cell_rows = []
    base_url = datasette.setting("base_url")
    for row in rows:
        cells = []
        # Unless we are a view, the first column is a link - either to the rowid
        # or to the simple or compound primary key
        if link_column:
            is_special_link_column = len(pks) != 1
            pk_path = path_from_row_pks(row, pks, not pks, False)
            cells.append({
                "column":
                pks[0] if len(pks) == 1 else "Link",
                "value_type":
                "pk",
                "is_special_link_column":
                is_special_link_column,
                "raw":
                pk_path,
                "value":
                markupsafe.Markup(
                    '<a href="{table_path}/{flat_pks_quoted}">{flat_pks}</a>'.
                    format(
                        base_url=base_url,
                        table_path=datasette.urls.table(
                            database_name, table_name),
                        flat_pks=str(markupsafe.escape(pk_path)),
                        flat_pks_quoted=path_from_row_pks(row, pks, not pks),
                    )),
            })

        for value, column_dict in zip(row, columns):
            column = column_dict["name"]
            if link_column and len(pks) == 1 and column == pks[0]:
                # If there's a simple primary key, don't repeat the value as it's
                # already shown in the link column.
                continue

            # First let the plugins have a go
            # pylint: disable=no-member
            plugin_display_value = None
            for candidate in pm.hook.render_cell(
                    value=value,
                    column=column,
                    table=table_name,
                    database=database_name,
                    datasette=datasette,
            ):
                candidate = await await_me_maybe(candidate)
                if candidate is not None:
                    plugin_display_value = candidate
                    break
            if plugin_display_value:
                display_value = plugin_display_value
            elif isinstance(value, bytes):
                formatted = format_bytes(len(value))
                display_value = markupsafe.Markup(
                    '<a class="blob-download" href="{}"{}>&lt;Binary:&nbsp;{:,}&nbsp;byte{}&gt;</a>'
                    .format(
                        datasette.urls.row_blob(
                            database_name,
                            table_name,
                            path_from_row_pks(row, pks, not pks),
                            column,
                        ),
                        ' title="{}"'.format(formatted)
                        if "bytes" not in formatted else "",
                        len(value),
                        "" if len(value) == 1 else "s",
                    ))
            elif isinstance(value, dict):
                # It's an expanded foreign key - display link to other row
                label = value["label"]
                value = value["value"]
                # The table we link to depends on the column
                other_table = column_to_foreign_key_table[column]
                link_template = LINK_WITH_LABEL if (
                    label != value) else LINK_WITH_VALUE
                display_value = markupsafe.Markup(
                    link_template.format(
                        database=database_name,
                        base_url=base_url,
                        table=tilde_encode(other_table),
                        link_id=tilde_encode(str(value)),
                        id=str(markupsafe.escape(value)),
                        label=str(markupsafe.escape(label)) or "-",
                    ))
            elif value in ("", None):
                display_value = markupsafe.Markup("&nbsp;")
            elif is_url(str(value).strip()):
                display_value = markupsafe.Markup(
                    '<a href="{url}">{url}</a>'.format(
                        url=markupsafe.escape(value.strip())))
            elif column in table_metadata.get("units", {}) and value != "":
                # Interpret units using pint
                value = value * ureg(table_metadata["units"][column])
                # Pint uses floating point which sometimes introduces errors in the compact
                # representation, which we have to round off to avoid ugliness. In the vast
                # majority of cases this rounding will be inconsequential. I hope.
                value = round(value.to_compact(), 6)
                display_value = markupsafe.Markup(f"{value:~P}".replace(
                    " ", "&nbsp;"))
            else:
                display_value = str(value)
                if truncate_cells and len(display_value) > truncate_cells:
                    display_value = display_value[:truncate_cells] + "\u2026"

            cells.append({
                "column":
                column,
                "value":
                display_value,
                "raw":
                value,
                "value_type":
                "none" if value is None else str(type(value).__name__),
            })
        cell_rows.append(Row(cells))

    if link_column:
        # Add the link column header.
        # If it's a simple primary key, we have to remove and re-add that column name at
        # the beginning of the header row.
        first_column = None
        if len(pks) == 1:
            columns = [col for col in columns if col["name"] != pks[0]]
            first_column = {
                "name": pks[0],
                "sortable": len(pks) == 1,
                "is_pk": True,
                "type": column_details[pks[0]].type,
                "notnull": column_details[pks[0]].notnull,
            }
        else:
            first_column = {
                "name": "Link",
                "sortable": False,
                "is_pk": False,
                "type": "",
                "notnull": 0,
            }
        columns = [first_column] + columns
    return columns, cell_rows
Ejemplo n.º 8
0
    async def display_columns_and_rows(
        self,
        database,
        table,
        description,
        rows,
        link_column=False,
    ):
        "Returns columns, rows for specified table - including fancy foreign key treatment"
        table_metadata = self.table_metadata(database, table)
        info = self.ds.inspect()[database]
        sortable_columns = self.sortable_columns_for_table(database, table, True)
        columns = [
            {"name": r[0], "sortable": r[0] in sortable_columns} for r in description
        ]
        tables = info["tables"]
        table_info = tables.get(table) or {}
        pks = table_info.get("primary_keys") or []
        column_to_foreign_key_table = {
            fk["column"]: fk["other_table"]
            for fk in table_info.get(
                "foreign_keys", {}
            ).get("outgoing", None) or []
        }

        cell_rows = []
        for row in rows:
            cells = []
            # Unless we are a view, the first column is a link - either to the rowid
            # or to the simple or compound primary key
            if link_column:
                cells.append(
                    {
                        "column": pks[0] if len(pks) == 1 else "Link",
                        "value": jinja2.Markup(
                            '<a href="/{database}/{table}/{flat_pks_quoted}">{flat_pks}</a>'.format(
                                database=database,
                                table=urllib.parse.quote_plus(table),
                                flat_pks=str(
                                    jinja2.escape(
                                        path_from_row_pks(row, pks, not pks, False)
                                    )
                                ),
                                flat_pks_quoted=path_from_row_pks(row, pks, not pks),
                            )
                        ),
                    }
                )

            for value, column_dict in zip(row, columns):
                column = column_dict["name"]
                if link_column and len(pks) == 1 and column == pks[0]:
                    # If there's a simple primary key, don't repeat the value as it's
                    # already shown in the link column.
                    continue

                if isinstance(value, dict):
                    # It's an expanded foreign key - display link to other row
                    label = value["label"]
                    value = value["value"]
                    # The table we link to depends on the column
                    other_table = column_to_foreign_key_table[column]
                    link_template = (
                        LINK_WITH_LABEL if (label != value) else LINK_WITH_VALUE
                    )
                    display_value = jinja2.Markup(link_template.format(
                        database=database,
                        table=urllib.parse.quote_plus(other_table),
                        link_id=urllib.parse.quote_plus(str(value)),
                        id=str(jinja2.escape(value)),
                        label=str(jinja2.escape(label)),
                    ))
                elif value in ("", None):
                    display_value = jinja2.Markup("&nbsp;")
                elif is_url(str(value).strip()):
                    display_value = jinja2.Markup(
                        '<a href="{url}">{url}</a>'.format(
                            url=jinja2.escape(value.strip())
                        )
                    )
                elif column in table_metadata.get("units", {}) and value != "":
                    # Interpret units using pint
                    value = value * ureg(table_metadata["units"][column])
                    # Pint uses floating point which sometimes introduces errors in the compact
                    # representation, which we have to round off to avoid ugliness. In the vast
                    # majority of cases this rounding will be inconsequential. I hope.
                    value = round(value.to_compact(), 6)
                    display_value = jinja2.Markup(
                        "{:~P}".format(value).replace(" ", "&nbsp;")
                    )
                else:
                    display_value = str(value)

                cells.append({"column": column, "value": display_value})
            cell_rows.append(cells)

        if link_column:
            # Add the link column header.
            # If it's a simple primary key, we have to remove and re-add that column name at
            # the beginning of the header row.
            if len(pks) == 1:
                columns = [col for col in columns if col["name"] != pks[0]]

            columns = [
                {"name": pks[0] if len(pks) == 1 else "Link", "sortable": len(pks) == 1}
            ] + columns
        return columns, cell_rows
Ejemplo n.º 9
0
    async def display_columns_and_rows(
        self,
        database,
        table,
        description,
        rows,
        link_column=False,
        expand_foreign_keys=True,
    ):
        "Returns columns, rows for specified table - including fancy foreign key treatment"
        table_metadata = self.table_metadata(database, table)
        info = self.ds.inspect()[database]
        sortable_columns = self.sortable_columns_for_table(
            database, table, True)
        columns = [{
            "name": r[0],
            "sortable": r[0] in sortable_columns
        } for r in description]
        tables = info["tables"]
        table_info = tables.get(table) or {}
        pks = table_info.get("primary_keys") or []

        # Prefetch foreign key resolutions for later expansion:
        fks = {}
        labeled_fks = {}
        if table_info and expand_foreign_keys:
            foreign_keys = table_info["foreign_keys"]["outgoing"]
            for fk in foreign_keys:
                label_column = (
                    # First look in metadata.json definition for this foreign key table:
                    self.table_metadata(database,
                                        fk["other_table"]).get("label_column")
                    # Fall back to label_column from .inspect() detection:
                    or tables.get(fk["other_table"], {}).get("label_column"))
                if not label_column:
                    # No label for this FK
                    fks[fk["column"]] = fk["other_table"]
                    continue

                ids_to_lookup = set([row[fk["column"]] for row in rows])
                sql = '''
                    select {other_column}, {label_column}
                    from {other_table}
                    where {other_column} in ({placeholders})
                '''.format(
                    other_column=escape_sqlite(fk["other_column"]),
                    label_column=escape_sqlite(label_column),
                    other_table=escape_sqlite(fk["other_table"]),
                    placeholders=", ".join(["?"] * len(ids_to_lookup)),
                )
                try:
                    results = await self.execute(database, sql,
                                                 list(set(ids_to_lookup)))
                except InterruptedError:
                    pass
                else:
                    for id, value in results:
                        labeled_fks[(fk["column"], id)] = (fk["other_table"],
                                                           value)

        cell_rows = []
        for row in rows:
            cells = []
            # Unless we are a view, the first column is a link - either to the rowid
            # or to the simple or compound primary key
            if link_column:
                cells.append({
                    "column":
                    pks[0] if len(pks) == 1 else "Link",
                    "value":
                    jinja2.Markup(
                        '<a href="/{database}/{table}/{flat_pks_quoted}">{flat_pks}</a>'
                        .format(
                            database=database,
                            table=urllib.parse.quote_plus(table),
                            flat_pks=str(
                                jinja2.escape(
                                    path_from_row_pks(row, pks, not pks,
                                                      False))),
                            flat_pks_quoted=path_from_row_pks(
                                row, pks, not pks),
                        )),
                })

            for value, column_dict in zip(row, columns):
                column = column_dict["name"]
                if link_column and len(pks) == 1 and column == pks[0]:
                    # If there's a simple primary key, don't repeat the value as it's
                    # already shown in the link column.
                    continue

                if (column, value) in labeled_fks:
                    other_table, label = labeled_fks[(column, value)]
                    display_value = jinja2.Markup(
                        '<a href="/{database}/{table}/{link_id}">{label}</a>&nbsp;<em>{id}</em>'
                        .format(
                            database=database,
                            table=urllib.parse.quote_plus(other_table),
                            link_id=urllib.parse.quote_plus(str(value)),
                            id=str(jinja2.escape(value)),
                            label=str(jinja2.escape(label)),
                        ))
                elif column in fks:
                    display_value = jinja2.Markup(
                        '<a href="/{database}/{table}/{link_id}">{id}</a>'.
                        format(
                            database=database,
                            table=urllib.parse.quote_plus(fks[column]),
                            link_id=urllib.parse.quote_plus(str(value)),
                            id=str(jinja2.escape(value)),
                        ))
                elif value is None:
                    display_value = jinja2.Markup("&nbsp;")
                elif is_url(str(value).strip()):
                    display_value = jinja2.Markup(
                        '<a href="{url}">{url}</a>'.format(
                            url=jinja2.escape(value.strip())))
                elif column in table_metadata.get("units", {}) and value != "":
                    # Interpret units using pint
                    value = value * ureg(table_metadata["units"][column])
                    # Pint uses floating point which sometimes introduces errors in the compact
                    # representation, which we have to round off to avoid ugliness. In the vast
                    # majority of cases this rounding will be inconsequential. I hope.
                    value = round(value.to_compact(), 6)
                    display_value = jinja2.Markup(
                        "{:~P}".format(value).replace(" ", "&nbsp;"))
                else:
                    display_value = str(value)

                cells.append({"column": column, "value": display_value})
            cell_rows.append(cells)

        if link_column:
            # Add the link column header.
            # If it's a simple primary key, we have to remove and re-add that column name at
            # the beginning of the header row.
            if len(pks) == 1:
                columns = [col for col in columns if col["name"] != pks[0]]

            columns = [{
                "name": pks[0] if len(pks) == 1 else "Link",
                "sortable": len(pks) == 1
            }] + columns
        return columns, cell_rows