Ticket #34924: models.py

File models.py, 11.0 KB (added by Sebastian Jekutsch, 8 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