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