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