Coverage for apis_core/apis_relations/tables.py: 55%
104 statements
« prev ^ index » next coverage.py v7.6.1, created at 2024-09-16 07:42 +0000
« prev ^ index » next coverage.py v7.6.1, created at 2024-09-16 07:42 +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"] = {
29 "viewname": "apis_core:apis_entities:generic_entities_list",
30 "args": [tables.A("model")],
31 }
32 super().__init__(*args, **kwargs)
35class PropertyTable(GenericTable):
36 """Construct table for properties.
38 The table shows how entities connect with one another via properties (relations).
39 It uses the format of an RDF triple – Subject-Predicate-Object – plus
40 "Reverse Predicate" for the inverse relationship and is displayed on the frontend
41 on the Relations > Property page.
42 """
44 # Note on constructing table columns / usage of variables:
45 # The variables used to declare table columns need to have the same names
46 # as the model field names from which the columns should be created,
47 # or tables.Column needs to contain an attribute "accessor" which references
48 # the original field name.
49 # For columns which allow sorting, the variable names are used as sort strings
50 # in the user's browser address bar, so for UX reasons, it may make sense to
51 # use different variable names than the original field names.
53 predicate = tables.Column(accessor="name_forward", verbose_name="Predicate")
54 predicate_reverse = tables.Column(
55 accessor="name_reverse", verbose_name="Reverse predicate"
56 )
57 subject = SubjObjColumn(accessor="subj_class", verbose_name="Subject")
58 object = SubjObjColumn(accessor="obj_class", verbose_name="Object")
60 class Meta(GenericTable.Meta):
61 fields = ["subject", "predicate", "object", "predicate_reverse"]
62 order_by = "predicate"
63 exclude = ["desc"]
64 sequence = tuple(fields) + GenericTable.Meta.sequence
66 # Use order_ methods to define how individual columns should be sorted.
67 # Method names are column names prefixed with "order_".
68 # By default, columns for regular fields are sorted alphabetically; for
69 # ManyToMany fields, however, the row IDs of the originating table are
70 # used as basis for sorting.
71 # When column names and field names differ (see earlier note), the original
72 # field names need to be referenced when constructing queryset.
73 def order_subject(self, queryset, is_descending):
74 queryset = queryset.annotate(entity=F("subj_class__model")).order_by(
75 ("-" if is_descending else "") + "entity"
76 )
77 return (queryset, True)
79 def order_object(self, queryset, is_descending):
80 queryset = queryset.annotate(entity=F("obj_class__model")).order_by(
81 ("-" if is_descending else "") + "entity"
82 )
83 return (queryset, True)
86class TripleTableBase(GenericTable):
87 """
88 The base table from which detail or edit tables will inherit from in order to avoid redundant definitions
89 """
91 class Meta:
92 model = TempTriple
94 # the fields list also serves as the defining order of them, as to avoid duplicated definitions
95 fields = [
96 "start_date_written",
97 "end_date_written",
98 "other_prop",
99 "other_entity",
100 "notes",
101 ]
102 exclude = (
103 "desc",
104 "view",
105 )
106 # reuse the list for ordering
107 sequence = tuple(fields)
109 def order_start_date_written(self, queryset, is_descending):
110 if is_descending:
111 return (queryset.order_by(F("start_date").desc(nulls_last=True)), True)
112 return (queryset.order_by(F("start_date").asc(nulls_last=True)), True)
114 def order_end_date_written(self, queryset, is_descending):
115 if is_descending:
116 return (queryset.order_by(F("end_date").desc(nulls_last=True)), True)
117 return (queryset.order_by(F("end_date").asc(nulls_last=True)), True)
119 def render_other_entity(self, record, value):
120 """
121 Custom render_FOO method for related entity linking. Since the 'other_related_entity' is a generated annotation
122 on the queryset, it does not return the related instance but only the foreign key as the integer it is.
123 Thus fetching the related instance is necessary.
125 :param record: The 'row' of a queryset, i.e. an entity instance
126 :param value: The current column of the row, i.e. the 'other_related_entity' annotation
127 :return: related instance
128 """
130 if value == record.subj.pk:
131 return record.subj
133 elif value == record.obj.pk:
134 return record.obj
136 else:
137 raise Exception(
138 "Did not find the entity this relation is supposed to come from!"
139 + "Something must have went wrong when annotating for the related instance."
140 )
142 def __init__(self, data, *args, **kwargs):
143 data = data.annotate(
144 other_entity=Case(
145 # **kwargs pattern is needed here as the key-value pairs change with each relation class and entity instance.
146 When(**{"subj__pk": self.entity_pk_self, "then": "obj"}),
147 When(**{"obj__pk": self.entity_pk_self, "then": "subj"}),
148 ),
149 other_prop=Case(
150 # **kwargs pattern is needed here as the key-value pairs change with each relation class and entity instance.
151 When(**{"subj__pk": self.entity_pk_self, "then": "prop__name_forward"}),
152 When(**{"obj__pk": self.entity_pk_self, "then": "prop__name_reverse"}),
153 ),
154 )
156 self.base_columns["other_prop"].verbose_name = "Other property"
157 self.base_columns[
158 "other_entity"
159 ].verbose_name = f"Related {self.other_entity_class_name.title()}"
161 super().__init__(data, *args, **kwargs)
163 def render_start_date_written(self, record, value):
164 if record.start_start_date is not None and record.start_end_date is not None:
165 title_text = f"{record.start_start_date} - {record.start_end_date}"
166 elif record.start_date is not None:
167 title_text = record.start_date
168 else:
169 return "—"
170 return format_html(f"<abbr title='{title_text}'>{value}</b>")
172 def render_end_date_written(self, record, value):
173 if record.end_start_date is not None and record.end_end_date is not None:
174 title_text = f"{record.end_start_date} - {record.end_end_date}"
175 elif record.end_date is not None:
176 title_text = record.end_date
177 else:
178 return "—"
179 return format_html(f"<abbr title='{title_text}'>{value}</b>")
182class TripleTableDetail(TripleTableBase):
183 class Meta(TripleTableBase.Meta):
184 exclude = TripleTableBase.Meta.exclude + ("delete", "edit")
186 def __init__(self, data, *args, **kwargs):
187 self.base_columns["other_entity"] = tables.Column(
188 linkify=lambda record: record.obj.get_absolute_url()
189 if record.other_entity == record.obj.id
190 else record.subj.get_absolute_url()
191 )
193 # bibsonomy button
194 if "apis_bibsonomy" in settings.INSTALLED_APPS:
195 self.base_columns["ref"] = tables.TemplateColumn(
196 template_name="apis_relations/references_button_generic_ajax_form.html"
197 )
199 super().__init__(data=data, *args, **kwargs)
202class TripleTableEdit(TripleTableBase):
203 class Meta(TripleTableBase.Meta):
204 fields = TripleTableBase.Meta.fields
205 if "apis_bibsonomy" in settings.INSTALLED_APPS:
206 fields = ["ref"] + TripleTableBase.Meta.fields
207 sequence = tuple(fields)
209 def __init__(self, *args, **kwargs):
210 self.base_columns["other_entity"] = tables.Column(
211 linkify=lambda record: record.obj.get_edit_url()
212 if record.other_entity == record.obj.id
213 else record.subj.get_edit_url()
214 )
216 self.base_columns["edit"] = tables.TemplateColumn(
217 template_name="apis_relations/edit_button_generic_ajax_form.html"
218 )
220 if "apis_bibsonomy" in settings.INSTALLED_APPS:
221 self.base_columns["ref"] = tables.TemplateColumn(
222 template_name="apis_relations/references_button_generic_ajax_form.html"
223 )
225 super().__init__(*args, **kwargs)
228def get_generic_triple_table(other_entity_class_name, entity_pk_self, detail):
229 if detail:
230 tt = TripleTableDetail
231 else:
232 tt = TripleTableEdit
233 tt.entity_pk_self = entity_pk_self
234 tt.other_entity_class_name = other_entity_class_name
235 return tt