import django_tables2 as tables
from django.conf import settings
from django.db.models import Case, F, When
from django.utils.html import format_html
from apis_core.apis_relations.models import TempTriple
from apis_core.generic.tables import GenericTable
empty_text_default = "There are currently no relations"
[docs]
class TripleTable(GenericTable):
subj = tables.Column(linkify=True)
obj = tables.Column(linkify=True)
[docs]
class SubjObjColumn(tables.ManyToManyColumn):
def __init__(self, *args, **kwargs):
kwargs["separator"] = format_html(",<br>")
kwargs["orderable"] = True
kwargs["filter"] = lambda qs: qs.order_by("model")
kwargs["transform"] = lambda ent: f"{ent.name.capitalize()}"
kwargs["linkify_item"] = lambda record: record.model_class().get_listview_url()
super().__init__(*args, **kwargs)
[docs]
class PropertyTable(GenericTable):
"""Construct table for properties.
The table shows how entities connect with one another via properties (relations).
It uses the format of an RDF triple – Subject-Predicate-Object – plus
"Reverse Predicate" for the inverse relationship and is displayed on the frontend
on the Relations > Property page.
"""
# Note on constructing table columns / usage of variables:
# The variables used to declare table columns need to have the same names
# as the model field names from which the columns should be created,
# or tables.Column needs to contain an attribute "accessor" which references
# the original field name.
# For columns which allow sorting, the variable names are used as sort strings
# in the user's browser address bar, so for UX reasons, it may make sense to
# use different variable names than the original field names.
predicate = tables.Column(accessor="name_forward", verbose_name="Predicate")
predicate_reverse = tables.Column(
accessor="name_reverse", verbose_name="Reverse predicate"
)
subject = SubjObjColumn(accessor="subj_class", verbose_name="Subject")
object = SubjObjColumn(accessor="obj_class", verbose_name="Object")
# Use order_ methods to define how individual columns should be sorted.
# Method names are column names prefixed with "order_".
# By default, columns for regular fields are sorted alphabetically; for
# ManyToMany fields, however, the row IDs of the originating table are
# used as basis for sorting.
# When column names and field names differ (see earlier note), the original
# field names need to be referenced when constructing queryset.
[docs]
def order_subject(self, queryset, is_descending):
queryset = queryset.annotate(entity=F("subj_class__model")).order_by(
("-" if is_descending else "") + "entity"
)
return (queryset, True)
[docs]
def order_object(self, queryset, is_descending):
queryset = queryset.annotate(entity=F("obj_class__model")).order_by(
("-" if is_descending else "") + "entity"
)
return (queryset, True)
[docs]
class TripleTableBase(GenericTable):
"""
The base table from which detail or edit tables will inherit from in order to avoid redundant definitions
"""
[docs]
def order_start_date_written(self, queryset, is_descending):
if is_descending:
return (queryset.order_by(F("start_date").desc(nulls_last=True)), True)
return (queryset.order_by(F("start_date").asc(nulls_last=True)), True)
[docs]
def order_end_date_written(self, queryset, is_descending):
if is_descending:
return (queryset.order_by(F("end_date").desc(nulls_last=True)), True)
return (queryset.order_by(F("end_date").asc(nulls_last=True)), True)
[docs]
def render_other_entity(self, record, value):
"""
Custom render_FOO method for related entity linking. Since the 'other_related_entity' is a generated annotation
on the queryset, it does not return the related instance but only the foreign key as the integer it is.
Thus fetching the related instance is necessary.
:param record: The 'row' of a queryset, i.e. an entity instance
:param value: The current column of the row, i.e. the 'other_related_entity' annotation
:return: related instance
"""
if value == record.subj.pk:
return record.subj
elif value == record.obj.pk:
return record.obj
else:
raise Exception(
"Did not find the entity this relation is supposed to come from!"
+ "Something must have went wrong when annotating for the related instance."
)
def __init__(self, data, *args, **kwargs):
data = data.annotate(
other_entity=Case(
# **kwargs pattern is needed here as the key-value pairs change with each relation class and entity instance.
When(**{"subj__pk": self.entity_pk_self, "then": "obj"}),
When(**{"obj__pk": self.entity_pk_self, "then": "subj"}),
),
other_prop=Case(
# **kwargs pattern is needed here as the key-value pairs change with each relation class and entity instance.
When(**{"subj__pk": self.entity_pk_self, "then": "prop__name_forward"}),
When(**{"obj__pk": self.entity_pk_self, "then": "prop__name_reverse"}),
),
)
self.base_columns["other_prop"].verbose_name = "Other property"
self.base_columns[
"other_entity"
].verbose_name = f"Related {self.other_entity_class_name.title()}"
super().__init__(data, *args, **kwargs)
[docs]
def render_start_date_written(self, record, value):
if record.start_start_date is not None and record.start_end_date is not None:
title_text = f"{record.start_start_date} - {record.start_end_date}"
elif record.start_date is not None:
title_text = record.start_date
else:
return "—"
return format_html(f"<abbr title='{title_text}'>{value}</b>")
[docs]
def render_end_date_written(self, record, value):
if record.end_start_date is not None and record.end_end_date is not None:
title_text = f"{record.end_start_date} - {record.end_end_date}"
elif record.end_date is not None:
title_text = record.end_date
else:
return "—"
return format_html(f"<abbr title='{title_text}'>{value}</b>")
[docs]
class TripleTableDetail(TripleTableBase):
def __init__(self, data, *args, **kwargs):
self.base_columns["other_entity"] = tables.Column(
linkify=lambda record: record.obj.get_absolute_url()
if record.other_entity == record.obj.id
else record.subj.get_absolute_url()
)
# bibsonomy button
if "apis_bibsonomy" in settings.INSTALLED_APPS:
self.base_columns["ref"] = tables.TemplateColumn(
template_name="apis_relations/references_button_generic_ajax_form.html"
)
super().__init__(data=data, *args, **kwargs)
[docs]
class TripleTableEdit(TripleTableBase):
def __init__(self, *args, **kwargs):
self.base_columns["other_entity"] = tables.Column(
linkify=lambda record: record.obj.get_edit_url()
if record.other_entity == record.obj.id
else record.subj.get_edit_url()
)
self.base_columns["edit"] = tables.TemplateColumn(
template_name="apis_relations/edit_button_generic_ajax_form.html"
)
if "apis_bibsonomy" in settings.INSTALLED_APPS:
self.base_columns["ref"] = tables.TemplateColumn(
template_name="apis_relations/references_button_generic_ajax_form.html"
)
super().__init__(*args, **kwargs)
[docs]
def get_generic_triple_table(other_entity_class_name, entity_pk_self, detail):
if detail:
tt = TripleTableDetail
else:
tt = TripleTableEdit
tt.entity_pk_self = entity_pk_self
tt.other_entity_class_name = other_entity_class_name
return tt