Ticket #34924: models.py

File models.py, 11.0 KB (added by Sebastian Jekutsch, 13 months ago)
Line 
1from textwrap import shorten
2from string import capwords
3from django.db import models
4from django.utils.text import slugify
5from django.core.validators import MaxValueValidator, MinValueValidator, FileExtensionValidator
6from django_group_by import GroupByMixin
7from django.db.models.functions import Lower
8
9# --------------------------------
10
11
12class NoUpperCaseCharField(models.CharField):
13
14 def __init__(self, *args, **kwargs):
15 super(NoUpperCaseCharField, self).__init__(*args, **kwargs)
16
17 def pre_save(self, model_instance, add):
18 value = getattr(model_instance, self.attname, None)
19 if value and str(value).isupper():
20 # value = capwords(str(value))
21 # Based on https://stackoverflow.com/a/42500863
22 value = ' '.join((w[:1].upper() + w[1:] if w not in ['for', 'and', 'in', 'against', 'to', 'on', 'the', 'of', 'für', 'und', 'der', 'die', 'das', 'über', 'von'] else w)
23 for w in str(value).lower().split(' '))
24 #value = value[0].upper + value[1:]
25 setattr(model_instance, self.attname, value)
26 return value
27 else:
28 return super(NoUpperCaseCharField, self).pre_save(model_instance, add)
29
30# --------------------------------
31
32
33class AbstractCategory(models.Model):
34 name = models.CharField(blank=False, max_length=50)
35 description = models.TextField(blank=True)
36
37 class Meta:
38 abstract = True
39 ordering = [Lower('name')]
40
41 def __str__(self) -> str:
42 return f"{self.name}"
43
44
45# TODO Alle Versuche, in diese Oberklasse auch noch die Category (und zusammen mit name auch __str__) reinzunehmen
46# sind bislang gescheitert. Die name könnten zudem verschieden lang sein bei den verschiedenen Entities
47class AbstractEntity(models.Model):
48 members = models.ManyToManyField('self', symmetrical=False, blank=True, related_name="memberships")
49 # level: int
50
51 class Meta:
52 abstract = True
53
54 # Returns members and it's sub-members alike, including self. Inspired by https://stackoverflow.com/questions/4725343/
55 # Sure this becomes quite slow in case of deep hierarchies. TODO Some prefetch or other Django tricks may help here
56 # TODO This is depth first search. Breadth first could enable a more relevant ordering of filter results.
57 # Having this, a level should be set for each member entity, see comments
58 # It used to use https://pypi.org/project/django-tree-queries/ which TreeNode has a parent and allows to call
59 # descendants(include_self=True) on all nodes to perform the same. But in in queries we need the children only.
60 # Other implementation like django-tree-queries, django-mptt (not supported anymore) or django-treebeard may support
61 # a more efficient implementation, but - apart from django-tree-queries - require much more effort in using it
62 def get_community(self): #(self, level: int = 0):
63 #self.level = level
64 result = {self}
65 for member in self.members.all():
66 result |= member.get_community() # (level+1)
67 return result
68
69 def get_near_community(self, level: int):
70 result = {self}
71 if level > 0:
72 for member in self.members.all():
73 result |= member.get_near_community(level-1)
74 return result
75
76 def get_community_level(self, level: int):
77 if level > 0:
78 if level == 1:
79 result = {member for member in self.members.all()}
80 else:
81 result = set()
82 for member in self.members.all():
83 result |= member.get_community_level(level-1)
84 else:
85 result = {self}
86 return result
87
88 def has_members(self) -> bool:
89 return self.members.count() > 0
90
91 def is_member(self) -> bool:
92 return self.memberships.count() > 0
93
94 def get_members(self):
95 # TODO Beware, this issues an SQL call for each row in the table
96 return [member for member in self.members.all()]
97
98 def get_memberships(self):
99 # TODO Beware, this issues an SQL call for each row in the table
100 return [membership for membership in self.memberships.all()]
101
102 # Used for prettier display in admin
103 has_members.boolean = True
104 is_member.boolean = True
105 get_members.short_description = "Members"
106 get_memberships.short_description = "Memberships"
107
108
109class ActorCategory(AbstractCategory):
110 class Meta(AbstractCategory.Meta):
111 verbose_name = "Actor Category"
112 verbose_name_plural = "Actor Categories"
113
114
115class Actor(AbstractEntity):
116 name = models.CharField(blank=False, max_length=100)
117 abbreviation = models.CharField(blank=True, max_length=20)
118 alternative_names = models.CharField(blank=True, max_length=250)
119 category = models.ForeignKey(ActorCategory, null=True, on_delete=models.DO_NOTHING)
120
121 class Meta:
122 ordering = [Lower('name')]
123 verbose_name = "Actor"
124 verbose_name_plural = "Actors"
125
126 def __str__(self) -> str:
127 return f"{self.abbreviation if self.abbreviation else self.name}"
128
129
130class ProductCategory(AbstractCategory):
131 class Meta(AbstractCategory.Meta):
132 verbose_name = "Product Category"
133 verbose_name_plural = "Product Categories"
134
135
136class Product(AbstractEntity):
137 name = models.CharField(blank=False, max_length=100)
138 alternative_names = models.CharField(blank=True, max_length=250)
139 category = models.ForeignKey(ProductCategory, null=True, on_delete=models.DO_NOTHING)
140
141 class Meta:
142 ordering = [Lower('name')]
143 verbose_name = "Product"
144 verbose_name_plural = "Products"
145
146 def __str__(self) -> str:
147 return f"{self.name} ({self.category})"
148
149
150class LocationCategory(AbstractCategory):
151 class Meta(AbstractCategory.Meta):
152 verbose_name = "Location Category"
153 verbose_name_plural = "Location Categories"
154
155
156class Location(AbstractEntity):
157 name = models.CharField(blank=False, max_length=100)
158 category = models.ForeignKey(LocationCategory, null=True, on_delete=models.DO_NOTHING)
159
160 class Meta:
161 ordering = [Lower('name')]
162 verbose_name = "Location"
163 verbose_name_plural = "Locations"
164
165 def __str__(self) -> str:
166 return f"{self.name} ({self.category})"
167
168
169class Impact(AbstractEntity):
170 name = models.CharField(blank=False, max_length=50)
171 alternative_names = models.CharField(blank=True, max_length=250)
172
173 class Meta:
174 ordering = [Lower('name')]
175 verbose_name = "Impact"
176 verbose_name_plural = "Impacts"
177
178 def __str__(self) -> str:
179 return f"{self.name}"
180
181
182class Activity(AbstractEntity):
183 name = models.CharField(blank=False, max_length=50)
184 alternative_names = models.CharField(blank=True, max_length=250)
185
186 class Meta:
187 ordering = [Lower('name')]
188 verbose_name = "Activity"
189 verbose_name_plural = "Activities"
190
191 def __str__(self) -> str:
192 return f"{self.name}"
193
194
195class Topic(models.Model):
196 products = models.ManyToManyField(Product, blank=True, related_name="topics")
197 locations = models.ManyToManyField(Location, blank=True, related_name="topics")
198 actors = models.ManyToManyField(Actor, blank=True, related_name="topics")
199 impacts = models.ManyToManyField(Impact, blank=True, related_name="topics")
200 activities = models.ManyToManyField(Activity, blank=True, related_name="topics")
201
202 class Meta:
203 verbose_name = "Topic"
204 verbose_name_plural = "Topics"
205
206 def __str__(self) -> str:
207 return f"{shorten(', '.join([str(p) for p in self.products.all()]), width=80, placeholder='...') or '-'} / " \
208 f"{shorten(', '.join([str(l) for l in self.locations.all()]), width=80, placeholder='...') or '-'} / " \
209 f"{shorten(', '.join([str(a) for a in self.actors.all()]), width=80, placeholder='...') or '-'} / " \
210 f"{shorten(', '.join([str(i) for i in self.impacts.all()]), width=80, placeholder='...') or '-'} / " \
211 f"{shorten(', '.join([str(a) for a in self.activities.all()]), width=80, placeholder='...') or '-'}"
212
213
214def upload_file(instance, filename):
215 file_path = "documents/{0}/{1}/{2}.{3}".format(
216 instance.release_date.year if instance.release_date else "",
217 instance.release_date.month if (instance.release_date and
218 (instance.release_date.day != 1 or instance.release_date.month != 1)) else "",
219 slugify(instance.title), "pdf")
220 return file_path
221
222
223class Document(models.Model):
224 title = NoUpperCaseCharField(blank=False, max_length=250)
225 subtitle = NoUpperCaseCharField(max_length=250, blank=True)
226 author = NoUpperCaseCharField(max_length=250, blank=True, verbose_name="Author(s)")
227 assocs = models.ManyToManyField(Actor, blank=True, verbose_name="Association")
228 release_date = models.DateField(blank=True, null=True)
229 volume = NoUpperCaseCharField(max_length=250, blank=True, help_text="Format-free reference to enclosing document")
230 source = models.URLField(blank=True, null=True)
231 abstract = models.TextField(blank=True)
232 file = models.FileField(blank=True, null=True, upload_to=upload_file, validators=[FileExtensionValidator(["pdf"])])
233 note = models.TextField(blank=True, help_text="Just some notes after reading the document")
234 todo = models.CharField(max_length=250, blank=True, help_text="Leftovers on tagging or reading this document")
235 topics = models.ManyToManyField(Topic, blank=True, through='Evidence', related_name="documents")
236 created_at = models.DateTimeField(auto_now_add=True)
237 updated_at = models.DateTimeField(auto_now=True)
238
239 class Meta:
240 ordering = ["release_date"]
241 verbose_name = "Document"
242 verbose_name_plural = "Documents"
243
244 def __str__(self) -> str:
245 return f"\"{self.title}\" by {','.join([str(a) for a in self.get_assocs()])}"
246
247 def get_assocs(self):
248 # TODO Beware, this issues an SQL call for each row in the table
249 return [assoc for assoc in self.assocs.all()]
250
251 get_assocs.short_description = "Associations"
252
253
254class EvidenceQuerySet(models.QuerySet, GroupByMixin):
255 pass
256
257
258class Evidence(models.Model):
259 document = models.ForeignKey(Document, on_delete=models.DO_NOTHING, related_name='evidences')
260 topic = models.ForeignKey(Topic, on_delete=models.DO_NOTHING, related_name='evidences')
261 reference = models.CharField(max_length=20, blank=True,
262 help_text="Section, chapter, page or similar reference within the document")
263 relevance = models.CharField(max_length=1, blank=True, choices=[
264 # Note that the keys (not the names) need to be in lexicographical order to help in sorting evidences
265 # TODO Replace with subclass of models.IntegerChoices
266 ("2", "High"),
267 ("5", "Useful"),
268 ("8", "Weak")
269 ])
270
271 objects = EvidenceQuerySet.as_manager()
272
273 class Meta:
274 ordering = ['relevance', '-document__release_date']
275 verbose_name = "Evidence"
276 verbose_name_plural = "Evidences" # Generally, there's no plural, but in this context...
277
278 def __str__(self) -> str:
279 return f"{self.topic} in \"{self.document.title}\""
Back to Top