Coverage for apis_core/apis_relations/tables.py: 55%
104 statements
« prev ^ index » next coverage.py v7.6.8, created at 2024-12-20 09:24 +0000
« prev ^ index » next coverage.py v7.6.8, created at 2024-12-20 09:24 +0000
1import django_tables2 as tables
2from django.conf import settings
3from django.db.models import Case, F, When
4from django.utils.html import format_html
6from apis_core.apis_relations.models import TempTriple
7from apis_core.generic.tables import GenericTable
9empty_text_default = "There are currently no relations"
12class TripleTable(GenericTable):
13 subj = tables.Column(linkify=True)
14 obj = tables.Column(linkify=True)
16 class Meta(GenericTable.Meta):
17 fields = ["id", "subj", "prop", "obj"]
18 exclude = ["desc"]
19 sequence = tuple(fields) + GenericTable.Meta.sequence
22class SubjObjColumn(tables.ManyToManyColumn):
23 def __init__(self, *args, **kwargs):
24 kwargs["separator"] = format_html(",<br>")
25 kwargs["orderable"] = True
26 kwargs["filter"] = lambda qs: qs.order_by("model")
27 kwargs["transform"] = lambda ent: f"{ent.name.capitalize()}"
28 kwargs["linkify_item"] = lambda record: record.model_class().get_listview_url()
29 super().__init__(*args, **kwargs)
32class PropertyTable(GenericTable):
33 """Construct table for properties.
35 The table shows how entities connect with one another via properties (relations).
36 It uses the format of an RDF triple – Subject-Predicate-Object – plus
37 "Reverse Predicate" for the inverse relationship and is displayed on the frontend
38 on the Relations > Property page.
39 """
41 # Note on constructing table columns / usage of variables:
42 # The variables used to declare table columns need to have the same names
43 # as the model field names from which the columns should be created,
44 # or tables.Column needs to contain an attribute "accessor" which references
45 # the original field name.
46 # For columns which allow sorting, the variable names are used as sort strings
47 # in the user's browser address bar, so for UX reasons, it may make sense to
48 # use different variable names than the original field names.
50 predicate = tables.Column(accessor="name_forward", verbose_name="Predicate")
51 predicate_reverse = tables.Column(
52 accessor="name_reverse", verbose_name="Reverse predicate"
53 )
54 subject = SubjObjColumn(accessor="subj_class", verbose_name="Subject")
55 object = SubjObjColumn(accessor="obj_class", verbose_name="Object")
57 class Meta(GenericTable.Meta):
58 fields = ["subject", "predicate", "object", "predicate_reverse"]
59 order_by = "predicate"
60 exclude = ["desc"]
61 sequence = tuple(fields) + GenericTable.Meta.sequence
63 # Use order_ methods to define how individual columns should be sorted.
64 # Method names are column names prefixed with "order_".
65 # By default, columns for regular fields are sorted alphabetically; for
66 # ManyToMany fields, however, the row IDs of the originating table are
67 # used as basis for sorting.
68 # When column names and field names differ (see earlier note), the original
69 # field names need to be referenced when constructing queryset.
70 def order_subject(self, queryset, is_descending):
71 queryset = queryset.annotate(entity=F("subj_class__model")).order_by(
72 ("-" if is_descending else "") + "entity"
73 )
74 return (queryset, True)
76 def order_object(self, queryset, is_descending):
77 queryset = queryset.annotate(entity=F("obj_class__model")).order_by(
78 ("-" if is_descending else "") + "entity"
79 )
80 return (queryset, True)
83class TripleTableBase(GenericTable):
84 """
85 The base table from which detail or edit tables will inherit from in order to avoid redundant definitions
86 """
88 class Meta:
89 model = TempTriple
91 # the fields list also serves as the defining order of them, as to avoid duplicated definitions
92 fields = [
93 "start_date_written",
94 "end_date_written",
95 "other_prop",
96 "other_entity",
97 "notes",
98 ]
99 exclude = (
100 "desc",
101 "view",
102 )
103 # reuse the list for ordering
104 sequence = tuple(fields)
106 def order_start_date_written(self, queryset, is_descending):
107 if is_descending:
108 return (queryset.order_by(F("start_date").desc(nulls_last=True)), True)
109 return (queryset.order_by(F("start_date").asc(nulls_last=True)), True)
111 def order_end_date_written(self, queryset, is_descending):
112 if is_descending:
113 return (queryset.order_by(F("end_date").desc(nulls_last=True)), True)
114 return (queryset.order_by(F("end_date").asc(nulls_last=True)), True)
116 def render_other_entity(self, record, value):
117 """
118 Custom render_FOO method for related entity linking. Since the 'other_related_entity' is a generated annotation
119 on the queryset, it does not return the related instance but only the foreign key as the integer it is.
120 Thus fetching the related instance is necessary.
122 :param record: The 'row' of a queryset, i.e. an entity instance
123 :param value: The current column of the row, i.e. the 'other_related_entity' annotation
124 :return: related instance
125 """
127 if value == record.subj.pk:
128 return record.subj
130 elif value == record.obj.pk:
131 return record.obj
133 else:
134 raise Exception(
135 "Did not find the entity this relation is supposed to come from!"
136 + "Something must have went wrong when annotating for the related instance."
137 )
139 def __init__(self, data, *args, **kwargs):
140 data = data.annotate(
141 other_entity=Case(
142 # **kwargs pattern is needed here as the key-value pairs change with each relation class and entity instance.
143 When(**{"subj__pk": self.entity_pk_self, "then": "obj"}),
144 When(**{"obj__pk": self.entity_pk_self, "then": "subj"}),
145 ),
146 other_prop=Case(
147 # **kwargs pattern is needed here as the key-value pairs change with each relation class and entity instance.
148 When(**{"subj__pk": self.entity_pk_self, "then": "prop__name_forward"}),
149 When(**{"obj__pk": self.entity_pk_self, "then": "prop__name_reverse"}),
150 ),
151 )
153 self.base_columns["other_prop"].verbose_name = "Other property"
154 self.base_columns[
155 "other_entity"
156 ].verbose_name = f"Related {self.other_entity_class_name.title()}"
158 super().__init__(data, *args, **kwargs)
160 def render_start_date_written(self, record, value):
161 if record.start_start_date is not None and record.start_end_date is not None:
162 title_text = f"{record.start_start_date} - {record.start_end_date}"
163 elif record.start_date is not None:
164 title_text = record.start_date
165 else:
166 return "—"
167 return format_html(f"<abbr title='{title_text}'>{value}</b>")
169 def render_end_date_written(self, record, value):
170 if record.end_start_date is not None and record.end_end_date is not None:
171 title_text = f"{record.end_start_date} - {record.end_end_date}"
172 elif record.end_date is not None:
173 title_text = record.end_date
174 else:
175 return "—"
176 return format_html(f"<abbr title='{title_text}'>{value}</b>")
179class TripleTableDetail(TripleTableBase):
180 class Meta(TripleTableBase.Meta):
181 exclude = TripleTableBase.Meta.exclude + ("delete", "edit")
183 def __init__(self, data, *args, **kwargs):
184 self.base_columns["other_entity"] = tables.Column(
185 linkify=lambda record: record.obj.get_absolute_url()
186 if record.other_entity == record.obj.id
187 else record.subj.get_absolute_url()
188 )
190 # bibsonomy button
191 if "apis_bibsonomy" in settings.INSTALLED_APPS:
192 self.base_columns["ref"] = tables.TemplateColumn(
193 template_name="apis_relations/references_button_generic_ajax_form.html"
194 )
196 super().__init__(data=data, *args, **kwargs)
199class TripleTableEdit(TripleTableBase):
200 class Meta(TripleTableBase.Meta):
201 fields = TripleTableBase.Meta.fields
202 if "apis_bibsonomy" in settings.INSTALLED_APPS:
203 fields = ["ref"] + TripleTableBase.Meta.fields
204 sequence = tuple(fields)
206 def __init__(self, *args, **kwargs):
207 self.base_columns["other_entity"] = tables.Column(
208 linkify=lambda record: record.obj.get_edit_url()
209 if record.other_entity == record.obj.id
210 else record.subj.get_edit_url()
211 )
213 self.base_columns["edit"] = tables.TemplateColumn(
214 template_name="apis_relations/edit_button_generic_ajax_form.html"
215 )
217 if "apis_bibsonomy" in settings.INSTALLED_APPS:
218 self.base_columns["ref"] = tables.TemplateColumn(
219 template_name="apis_relations/references_button_generic_ajax_form.html"
220 )
222 super().__init__(*args, **kwargs)
225def get_generic_triple_table(other_entity_class_name, entity_pk_self, detail):
226 if detail:
227 tt = TripleTableDetail
228 else:
229 tt = TripleTableEdit
230 tt.entity_pk_self = entity_pk_self
231 tt.other_entity_class_name = other_entity_class_name
232 return tt