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

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"] = { 

29 "viewname": "apis_core:apis_entities:generic_entities_list", 

30 "args": [tables.A("model")], 

31 } 

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

33 

34 

35class PropertyTable(GenericTable): 

36 """Construct table for properties. 

37 

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

43 

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. 

52 

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

59 

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 

65 

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) 

78 

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) 

84 

85 

86class TripleTableBase(GenericTable): 

87 """ 

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

89 """ 

90 

91 class Meta: 

92 model = TempTriple 

93 

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) 

108 

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) 

113 

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) 

118 

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. 

124 

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

129 

130 if value == record.subj.pk: 

131 return record.subj 

132 

133 elif value == record.obj.pk: 

134 return record.obj 

135 

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 ) 

141 

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 ) 

155 

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

160 

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

162 

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

171 

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

180 

181 

182class TripleTableDetail(TripleTableBase): 

183 class Meta(TripleTableBase.Meta): 

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

185 

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 ) 

192 

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 ) 

198 

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

200 

201 

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) 

208 

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 ) 

215 

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

217 template_name="apis_relations/edit_button_generic_ajax_form.html" 

218 ) 

219 

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 ) 

224 

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

226 

227 

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