Tests.
[redakcja.git] / src / catalogue / models.py
1 from collections import Counter
2 import decimal
3 from django.apps import apps
4 from django.db import models
5 from django.urls import reverse
6 from django.utils.translation import gettext_lazy as _
7 from wikidata.client import Client
8 from .constants import WIKIDATA
9 from .wikidata import WikidataMixin
10
11
12 class Author(WikidataMixin, models.Model):
13     slug = models.SlugField(max_length=255, null=True, blank=True, unique=True)
14     first_name = models.CharField(_("first name"), max_length=255, blank=True)
15     last_name = models.CharField(_("last name"), max_length=255, blank=True)
16
17     name_de = models.CharField(_("name (de)"), max_length=255, blank=True)
18     name_lt = models.CharField(_("name (lt)"), max_length=255, blank=True)
19
20     gender = models.CharField(_("gender"), max_length=255, blank=True)
21     nationality = models.CharField(_("nationality"), max_length=255, blank=True)
22     year_of_death = models.SmallIntegerField(_("year of death"), null=True, blank=True)
23     status = models.PositiveSmallIntegerField(
24         _("status"), 
25         null=True,
26         blank=True,
27         choices=[
28             (1, _("Alive")),
29             (2, _("Dead")),
30             (3, _("Long dead")),
31             (4, _("Unknown")),
32         ],
33     )
34     notes = models.TextField(_("notes"), blank=True)
35     gazeta_link = models.CharField(_("gazeta link"), max_length=255, blank=True)
36     culturepl_link = models.CharField(_("culture.pl link"), max_length=255, blank=True)
37
38     description = models.TextField(_("description"), blank=True)
39     description_de = models.TextField(_("description (de)"), blank=True)
40     description_lt = models.TextField(_("description (lt)"), blank=True)
41
42     priority = models.PositiveSmallIntegerField(
43         _("priority"), 
44         default=0, choices=[(0, _("Low")), (1, _("Medium")), (2, _("High"))]
45     )
46     collections = models.ManyToManyField("Collection", blank=True, verbose_name=_("collections"))
47
48     class Meta:
49         verbose_name = _('author')
50         verbose_name_plural = _('authors')
51         ordering = ("last_name", "first_name", "year_of_death")
52
53     class Wikidata:
54         first_name = WIKIDATA.GIVEN_NAME
55         last_name = WIKIDATA.LAST_NAME
56         year_of_death = WIKIDATA.DATE_OF_DEATH
57         gender = WIKIDATA.GENDER
58         notes = "description"
59
60     def __str__(self):
61         return f"{self.first_name} {self.last_name}"
62
63     def get_absolute_url(self):
64         return reverse("catalogue_author", args=[self.slug])
65
66     @property
67     def name(self):
68         return f"{self.last_name}, {self.first_name}"
69     
70     @property
71     def pd_year(self):
72         if self.year_of_death:
73             return self.year_of_death + 71
74         elif self.year_of_death == 0:
75             return 0
76         else:
77             return None
78
79
80 class Category(WikidataMixin, models.Model):
81     name = models.CharField(_("name"), max_length=255)
82     slug = models.SlugField(max_length=255, unique=True)
83
84     class Meta:
85         abstract = True
86
87     def __str__(self):
88         return self.name
89
90 class Epoch(Category):
91     class Meta:
92         verbose_name = _('epoch')
93         verbose_name_plural = _('epochs')
94
95
96 class Genre(Category):
97     class Meta:
98         verbose_name = _('genre')
99         verbose_name_plural = _('genres')
100
101
102 class Kind(Category):
103     class Meta:
104         verbose_name = _('kind')
105         verbose_name_plural = _('kinds')
106
107
108 class Book(WikidataMixin, models.Model):
109     slug = models.SlugField(max_length=255, blank=True, null=True, unique=True)
110     authors = models.ManyToManyField(Author, blank=True, verbose_name=_("authors"))
111     translators = models.ManyToManyField(
112         Author,
113         related_name="translated_book_set",
114         related_query_name="translated_book",
115         blank=True,
116         verbose_name=_("translators")
117     )
118     epochs = models.ManyToManyField(Epoch, blank=True, verbose_name=_("epochs"))
119     kinds = models.ManyToManyField(Kind, blank=True, verbose_name=_("kinds"))
120     genres = models.ManyToManyField(Genre, blank=True, verbose_name=_("genres"))
121     title = models.CharField(_("title"), max_length=255, blank=True)
122     language = models.CharField(_("language"), max_length=255, blank=True)
123     based_on = models.ForeignKey(
124         "self", models.PROTECT, related_name="translation", null=True, blank=True,
125         verbose_name=_("based on")
126     )
127     scans_source = models.CharField(_("scans source"), max_length=255, blank=True)
128     text_source = models.CharField(_("text source"), max_length=255, blank=True)
129     notes = models.TextField(_("notes"), blank=True)
130     priority = models.PositiveSmallIntegerField(
131         _("priority"),
132         default=0, choices=[(0, _("Low")), (1, _("Medium")), (2, _("High"))]
133     )
134     pd_year = models.IntegerField(_('year of entry into PD'), null=True, blank=True)
135     gazeta_link = models.CharField(_("gazeta link"), max_length=255, blank=True)
136     collections = models.ManyToManyField("Collection", blank=True, verbose_name=_("collections"))
137
138     estimated_chars = models.IntegerField(_("estimated number of characters"), null=True, blank=True)
139     estimated_verses = models.IntegerField(_("estimated number of verses"), null=True, blank=True)
140     estimate_source = models.CharField(_("source of estimates"), max_length=2048, blank=True)
141
142     class Meta:
143         ordering = ("title",)
144         verbose_name = _('book')
145         verbose_name_plural = _('books')
146
147     class Wikidata:
148         authors = WIKIDATA.AUTHOR
149         translators = WIKIDATA.TRANSLATOR
150         title = WIKIDATA.TITLE
151         language = WIKIDATA.LANGUAGE
152         based_on = WIKIDATA.BASED_ON
153         notes = "description"
154
155     def __str__(self):
156         txt = self.title
157         astr = self.authors_str()
158         if astr:
159             txt = f"{astr} – {txt}"
160         tstr = self.translators_str()
161         if tstr:
162             txt = f"{txt} (tłum. {tstr})"
163         return txt
164
165     def get_absolute_url(self):
166         return reverse("catalogue_book", args=[self.slug])
167
168     @property
169     def wluri(self):
170         return f'https://wolnelektury.pl/katalog/lektura/{self.slug}/'
171     
172     def authors_str(self):
173         return ", ".join(str(author) for author in self.authors.all())
174     authors_str.admin_order_field = 'authors__last_name'
175     authors_str.short_description = _('Author')
176
177     def translators_str(self):
178         return ", ".join(str(author) for author in self.translators.all())
179     translators_str.admin_order_field = 'translators__last_name'
180     translators_str.short_description = _('Translator')
181
182     def get_estimated_costs(self):
183         return {
184             work_type: work_type.calculate(self)
185             for work_type in WorkType.objects.all()
186         }
187
188
189 class CollectionCategory(models.Model):
190     name = models.CharField(_("name"), max_length=255)
191     parent = models.ForeignKey('self', models.SET_NULL, related_name='children', null=True, blank=True, verbose_name=_("parent"))
192     notes = models.TextField(_("notes"), blank=True)
193
194     class Meta:
195         ordering = ('parent__name', 'name')
196         verbose_name = _('collection category')
197         verbose_name_plural = _('collection categories')
198
199     def __str__(self):
200         if self.parent:
201             return f"{self.parent} / {self.name}"
202         else:
203             return self.name
204
205
206 class Collection(models.Model):
207     name = models.CharField(_("name"), max_length=255)
208     slug = models.SlugField(max_length=255, unique=True)
209     category = models.ForeignKey(CollectionCategory, models.SET_NULL, null=True, blank=True, verbose_name=_("category"))
210     notes = models.TextField(_("notes"), blank=True)
211
212     class Meta:
213         ordering = ('category', 'name')
214         verbose_name = _('collection')
215         verbose_name_plural = _('collections')
216
217     def __str__(self):
218         if self.category:
219             return f"{self.category} / {self.name}"
220         else:
221             return self.name
222
223     def get_estimated_costs(self):
224         costs = Counter()
225         for book in self.book_set.all():
226             for k, v in book.get_estimated_costs().items():
227                 costs[k] += v or 0
228
229         for author in self.author_set.all():
230             for book in author.book_set.all():
231                 for k, v in book.get_estimated_costs().items():
232                     costs[k] += v or 0
233             for book in author.translated_book_set.all():
234                 for k, v in book.get_estimated_costs().items():
235                     costs[k] += v or 0
236         return costs
237
238
239 class WorkType(models.Model):
240     name = models.CharField(_("name"), max_length=255)
241
242     class Meta:
243         ordering = ('name',)
244         verbose_name = _('work type')
245         verbose_name_plural = _('work types')
246     
247     def get_rate_for(self, book):
248         for workrate in self.workrate_set.all():
249             if workrate.matches(book):
250                 return workrate
251
252     def calculate(self, book):
253         workrate = self.get_rate_for(book)
254         if workrate is not None:
255             return workrate.calculate(book)
256         
257
258
259 class WorkRate(models.Model):
260     priority = models.IntegerField(_("priority"), default=1)
261     per_normpage = models.DecimalField(_("per normalized page"), decimal_places=2, max_digits=6, null=True, blank=True)
262     per_verse = models.DecimalField(_("per verse"), decimal_places=2, max_digits=6, null=True, blank=True)
263     work_type = models.ForeignKey(WorkType, models.CASCADE, verbose_name=_("work type"))
264     epochs = models.ManyToManyField(Epoch, blank=True, verbose_name=_("epochs"))
265     kinds = models.ManyToManyField(Kind, blank=True, verbose_name=_("kinds"))
266     genres = models.ManyToManyField(Genre, blank=True, verbose_name=_("genres"))
267     collections = models.ManyToManyField(Collection, blank=True, verbose_name=_("collections"))
268
269     class Meta:
270         ordering = ('priority',)
271         verbose_name = _('work rate')
272         verbose_name_plural = _('work rates')
273
274     def matches(self, book):
275         for category in 'epochs', 'kinds', 'genres', 'collections':
276             oneof = getattr(self, category).all()
277             if oneof:
278                 if not set(oneof).intersection(
279                         getattr(book, category).all()):
280                     return False
281         return True
282
283     def calculate(self, book):
284         if self.per_verse:
285             if book.estimated_verses:
286                 return book.estimated_verses * self.per_verse
287         elif self.per_normpage:
288             if book.estimated_chars:
289                 return (decimal.Decimal(book.estimated_chars) / 1800 * self.per_normpage).quantize(decimal.Decimal('1.00'), rounding=decimal.ROUND_HALF_UP)
290