Source code for fairdm.contrib.collections.tables

"""Table classes and column helpers for sample and measurement listings."""

import django_tables2 as tables
from controlled_vocabularies.fields import MultiConceptField
from django.core.exceptions import FieldDoesNotExist
from django.utils.html import format_html, format_html_join
from django.utils.translation import gettext_lazy as _
from easy_icons import icon

from fairdm.utils.choices import Visibility


[docs] def render_concept_many_to_many(value): """Render a concept many-to-many value as comma-separated links. Args: value: The related manager for the concept field. Returns: HTML with one link per concept, or an empty string when there are none. """ if not value: return "" return format_html_join( ", ", '<a href="{}">{}</a>', ((concept.uri, concept.display_label()) for concept in value.all()), )
field_map = { "CharField": "char", "TextField": "char", "IntegerField": "num", "BigIntegerField": "num", "PositiveIntegerField": "num", "PositiveSmallIntegerField": "num", "SmallIntegerField": "num", "BooleanField": "bool", "DateField": "date", "DateTimeField": "datetime", "TimeField": "time", "DecimalField": "num", "FloatField": "num", "ForeignKey": "rel", "ManyToManyField": "rel", }
[docs] class BaseTable(tables.Table): """Base table for all listings, adding type-based column classes and header tooltips. Args: *args: Passed to ``django_tables2.Table``. **kwargs: Passed to ``django_tables2.Table``. Attributes: id: The hidden UUID column. dataset: The dataset icon column. """ id = tables.Column(verbose_name="UUID", visible=False) dataset = tables.Column(orderable=False, verbose_name="")
[docs] def render_dataset(self, value): """Link the dataset icon unless the dataset is private. A published dataset can still be private, and its records stay in the listing without a link to a page the visitor cannot read. """ if value.visibility != Visibility.PRIVATE: return format_html( '<a href="{}">{}</a>', value.get_absolute_url(), icon("dataset") ) return icon("dataset")
[docs] def value_dataset(self, value): """Export the dataset's UUID.""" return value.uuid
def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.base_columns["id"].visible = False self.configure_column_attrs() self.update_concept_field_render_methods()
[docs] def configure_column_attrs(self): """Set each column's type and name CSS classes and its header tooltip from the model field.""" model = getattr(self._meta, "model", None) for bound_col in self.columns: col = getattr(bound_col, "column", bound_col) # A dotted accessor such as 'sample.location.x' looks up its first part. fname = getattr(col, "accessor", None) or getattr(col, "name", "") field_name_for_lookup = fname.split(".")[0] if fname else "" db_field = None field_type = "CharField" if model and field_name_for_lookup: try: db_field = model._meta.get_field(field_name_for_lookup) field_type = db_field.get_internal_type() except Exception: field_type = "CharField" field_type_class = field_map.get(field_type, "char") field_name_class = f"col-{fname.replace('_', '-')}" if fname else "" classes = f"{field_type_class} {field_name_class}".strip() td = col.attrs.setdefault("td", {}) current = td.get("class", "") td["class"] = f"{classes} {current if current else ''}".strip() th = col.attrs.setdefault("th", {}) current_th = th.get("class", "") th["class"] = f"{classes} {current_th if current_th else ''}".strip() help_text = getattr(db_field, "help_text", "") if db_field else "" if help_text: th["title"] = str(help_text)
[docs] def update_concept_field_render_methods(self): """Render concept many-to-many columns as links.""" for c in self.columns.columns.values(): try: field = self._meta.model._meta.get_field(c.accessor) except FieldDoesNotExist: continue if isinstance(field, MultiConceptField): c.render = render_concept_many_to_many
[docs] class SampleTable(BaseTable): """Listing table for samples.""" name = tables.Column(linkify=True) # A geometry field has no `location__y` lookup to order by. latitude = tables.Column( accessor="location.y", verbose_name=_("Latitude"), orderable=False ) longitude = tables.Column( accessor="location.x", verbose_name=_("Longitude"), orderable=False )
[docs] class Meta: attrs = { "class": "table table-striped table-hover overflow-auto align-middle mb-0" } sequence = ("dataset", "...") # `added` is not unique, so paging needs `id` as a tie-break. `id` is always a column. order_by = ("added", "id")
[docs] class MeasurementTable(BaseTable): """Listing table for measurements.""" name = tables.Column(linkify=True, verbose_name=_("Name")) sample = tables.Column() # The queryset carries `effective_location` from `with_effective_location(user)`: the # measurement's own point, else its sample's for a viewer who may open the sample. latitude = tables.Column( accessor="effective_location.y", verbose_name=_("Latitude"), orderable=False ) longitude = tables.Column( accessor="effective_location.x", verbose_name=_("Longitude"), orderable=False )
[docs] class Meta: attrs = { "class": "table table-striped table-hover overflow-auto align-middle mb-0" } # `modified` is not unique, so paging needs `id` as a tie-break. order_by = ("-modified", "id")
[docs] def render_sample(self, value): """Link the sample only when its dataset is published.""" if value.dataset.published: return format_html('<a href="{}">{}</a>', value.get_absolute_url(), value) return _("Unpublished")