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

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 

5 

6from apis_core.apis_relations.models import TempTriple 

7from apis_core.generic.tables import GenericTable 

8 

9empty_text_default = "There are currently no relations" 

10 

11 

12class TripleTable(GenericTable): 

13 subj = tables.Column(linkify=True) 

14 obj = tables.Column(linkify=True) 

15 

16 class Meta(GenericTable.Meta): 

17 fields = ["id", "subj", "prop", "obj"] 

18 exclude = ["desc"] 

19 sequence = tuple(fields) + GenericTable.Meta.sequence 

20 

21 

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) 

30 

31 

32class PropertyTable(GenericTable): 

33 """Construct table for properties. 

34 

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 """ 

40 

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. 

49 

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") 

56 

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 

62 

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) 

75 

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) 

81 

82 

83class TripleTableBase(GenericTable): 

84 """ 

85 The base table from which detail or edit tables will inherit from in order to avoid redundant definitions 

86 """ 

87 

88 class Meta: 

89 model = TempTriple 

90 

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) 

105 

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) 

110 

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) 

115 

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. 

121 

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 """ 

126 

127 if value == record.subj.pk: 

128 return record.subj 

129 

130 elif value == record.obj.pk: 

131 return record.obj 

132 

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 ) 

138 

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 ) 

152 

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()}" 

157 

158 super().__init__(data, *args, **kwargs) 

159 

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>") 

168 

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>") 

177 

178 

179class TripleTableDetail(TripleTableBase): 

180 class Meta(TripleTableBase.Meta): 

181 exclude = TripleTableBase.Meta.exclude + ("delete", "edit") 

182 

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 ) 

189 

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 ) 

195 

196 super().__init__(data=data, *args, **kwargs) 

197 

198 

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) 

205 

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 ) 

212 

213 self.base_columns["edit"] = tables.TemplateColumn( 

214 template_name="apis_relations/edit_button_generic_ajax_form.html" 

215 ) 

216 

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 ) 

221 

222 super().__init__(*args, **kwargs) 

223 

224 

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