Fundraising in PDF.
[wolnelektury.git] / src / catalogue / models / tag.py
1 # This file is part of Wolne Lektury, licensed under GNU Affero GPLv3 or later.
2 # Copyright © Fundacja Wolne Lektury. See NOTICE for more information.
3 #
4 from django.conf import settings
5 from django.contrib.contenttypes.fields import GenericForeignKey
6 from django.contrib.contenttypes.models import ContentType
7 from django.core.cache import caches
8 from django.contrib.auth.models import User
9 from django.core.exceptions import ObjectDoesNotExist
10 from django.db import models
11 from django.db.models.query import Prefetch
12 from django.dispatch import Signal
13 from django.urls import reverse
14 from django.utils.translation import gettext_lazy as _
15
16 from newtagging.models import TagManager, TaggedItemManager
17
18
19 TAG_CATEGORIES = (
20     ('author', _('autor')),
21     ('epoch', _('epoka')),
22     ('kind', _('rodzaj')),
23     ('genre', _('gatunek')),
24     ('theme', _('motyw')),
25     ('set', _('półka')),
26     ('thing', _('obiekt')),  # things shown on pictures
27 )
28
29
30 class TagRelation(models.Model):
31     tag = models.ForeignKey('Tag', models.CASCADE, verbose_name='tag', related_name='items')
32     content_type = models.ForeignKey(ContentType, models.CASCADE, verbose_name='typ obiektu')
33     object_id = models.PositiveIntegerField('id obiektu', db_index=True)
34     content_object = GenericForeignKey('content_type', 'object_id')
35
36     objects = TaggedItemManager()
37
38     class Meta:
39         db_table = 'catalogue_tag_relation'
40         unique_together = (('tag', 'content_type', 'object_id'),)
41
42     def __str__(self):
43         try:
44             return '%s [%s]' % (self.content_type.get_object_for_this_type(pk=self.object_id), self.tag)
45         except ObjectDoesNotExist:
46             return '<deleted> [%s]' % self.tag
47
48
49 class Tag(models.Model):
50     """A tag attachable to books and fragments (and possibly anything).
51
52     Used to represent searchable metadata (authors, epochs, genres, kinds),
53     fragment themes (motifs) and some book hierarchy related kludges."""
54     name = models.CharField('nazwa', max_length=120, db_index=True)
55     slug = models.SlugField('slug', max_length=120, db_index=True)
56     sort_key = models.CharField('klucz sortowania', max_length=120, db_index=True)
57     category = models.CharField(
58         'kategoria', max_length=50, blank=False, null=False, db_index=True, choices=TAG_CATEGORIES)
59     description = models.TextField('opis', blank=True)
60
61     for_books = models.BooleanField(default=False)
62     for_pictures = models.BooleanField(default=False)
63
64     user = models.ForeignKey(User, models.CASCADE, blank=True, null=True)
65     gazeta_link = models.CharField(blank=True, max_length=240)
66     culturepl_link = models.CharField(blank=True, max_length=240)
67     wiki_link = models.CharField(blank=True, max_length=240)
68     photo = models.FileField(blank=True, null=True, upload_to='catalogue/tag/')
69     photo_attribution = models.CharField(max_length=255, blank=True)
70
71     created_at = models.DateTimeField('data utworzenia', auto_now_add=True, db_index=True)
72     changed_at = models.DateTimeField('data modyfikacji', auto_now=True, db_index=True)
73
74     plural = models.CharField(
75         'liczba mnoga', max_length=255, blank=True,
76         help_text='dotyczy gatunków'
77     )
78     genre_epoch_specific = models.BooleanField(
79         default=False,
80         help_text='Po wskazaniu tego gatunku, dodanie epoki byłoby nadmiarowe, np. „dramat romantyczny”'
81     )
82     adjective_feminine_singular = models.CharField(
83         'przymiotnik pojedynczy żeński', max_length=255, blank=True,
84         help_text='twórczość … Adama Mickiewicza; dotyczy epok'
85     )
86     adjective_nonmasculine_plural = models.CharField(
87         'przymiotnik mnogi niemęskoosobowy', max_length=255, blank=True,
88         help_text='utwory … Adama Mickiewicza; dotyczy epok'
89     )
90     genitive = models.CharField(
91         'dopełniacz', max_length=255, blank=True,
92         help_text='utwory … (czyje?); dotyczy autorów'
93     )
94     collective_noun = models.CharField(
95         'określenie zbiorowe', max_length=255, blank=True,
96         help_text='np. „Liryka” albo „Twórczość dramatyczna”; dotyczy rodzajów'
97     )
98
99     after_change = Signal()
100
101     intermediary_table_model = TagRelation
102     objects = TagManager()
103
104     class UrlDeprecationWarning(DeprecationWarning):
105         def __init__(self, tags=None):
106             super(Tag.UrlDeprecationWarning, self).__init__()
107             self.tags = tags
108
109     categories_rev = {
110         'autor': 'author',
111         'epoka': 'epoch',
112         'rodzaj': 'kind',
113         'gatunek': 'genre',
114         'motyw': 'theme',
115         'polka': 'set',
116         'obiekt': 'thing',
117     }
118     categories_dict = dict((item[::-1] for item in categories_rev.items()))
119
120     class Meta:
121         ordering = ('sort_key',)
122         verbose_name = 'tag'
123         verbose_name_plural = 'tagi'
124         unique_together = (("slug", "category"),)
125         app_label = 'catalogue'
126
127     def save(self, *args, quick=False, **kwargs):
128         existing = self.pk and self.category != 'set'
129         ret = super(Tag, self).save(*args, **kwargs)
130         if existing and not quick:
131             self.after_change.send(sender=type(self), instance=self)
132         return ret
133
134     def __str__(self):
135         return self.name
136
137     def __repr__(self):
138         return "Tag(slug=%r)" % self.slug
139
140     def get_initial(self):
141         if self.category == 'author':
142             return self.sort_key[0]
143         elif self.name:
144             return self.name[0]
145         else:
146             return ''
147
148     @property
149     def category_plural(self):
150         return self.category + 's'
151
152     def get_absolute_url(self):
153         return reverse('tagged_object_list', args=[self.url_chunk])
154
155     def get_absolute_gallery_url(self):
156         return reverse('tagged_object_list_gallery', args=[self.url_chunk])
157
158     def get_absolute_catalogue_url(self):
159         # TODO: remove magic.
160         if self.category == 'set':
161             return reverse('social_my_shelf')
162         elif self.category == 'thing':
163             return ''
164         else:
165             return reverse(f'{self.category}_catalogue')
166
167     def has_description(self):
168         return len(self.description) > 0
169     has_description.short_description = 'opis'
170     has_description.boolean = True
171
172     @staticmethod
173     def get_tag_list(tag_str):
174         if not tag_str:
175             return []
176         tags = []
177         ambiguous_slugs = []
178         category = None
179         deprecated = False
180         tags_splitted = tag_str.split('/')
181         for name in tags_splitted:
182             if category:
183                 tags.append(Tag.objects.get(slug=name, category=category))
184                 category = None
185             elif name in Tag.categories_rev:
186                 category = Tag.categories_rev[name]
187             else:
188                 try:
189                     tags.append(Tag.objects.get(slug=name))
190                     deprecated = True
191                 except Tag.MultipleObjectsReturned:
192                     ambiguous_slugs.append(name)
193
194         if category:
195             # something strange left off
196             raise Tag.DoesNotExist()
197         if ambiguous_slugs:
198             # some tags should be qualified
199             e = Tag.MultipleObjectsReturned()
200             e.tags = tags
201             e.ambiguous_slugs = ambiguous_slugs
202             raise e
203         if deprecated:
204             raise Tag.UrlDeprecationWarning(tags=tags)
205         return tags
206
207     @property
208     def url_chunk(self):
209         return '/'.join((Tag.categories_dict[self.category], self.slug))
210
211     @staticmethod
212     def tags_from_info(info):
213         from slugify import slugify
214         from sortify import sortify
215         meta_tags = []
216         categories = (('kinds', 'kind'), ('genres', 'genre'), ('authors', 'author'), ('epochs', 'epoch'))
217         for field_name, category in categories:
218             try:
219                 tag_names = getattr(info, field_name)
220             except (AttributeError, KeyError):  # TODO: shouldn't be KeyError here at all.
221                 try:
222                     tag_names = [getattr(info, category)]
223                 except KeyError:
224                     # For instance, Pictures do not have 'genre' field.
225                     continue
226             for tag_name in tag_names:
227                 lang = getattr(tag_name, 'lang', None) or settings.LANGUAGE_CODE
228                 tag_sort_key = tag_name
229                 if category == 'author':
230                     tag_sort_key = ' '.join((tag_name.last_name,) + tag_name.first_names)
231                     tag_name = tag_name.readable()
232
233                 try:
234                     tag = Tag.objects.get(category=category, **{"name_%s" % lang: tag_name})
235                 except Tag.DoesNotExist:
236                     if lang == settings.LANGUAGE_CODE:
237                         # Allow creating new tag, if it's in default language.
238                         tag, created = Tag.objects.get_or_create(slug=slugify(tag_name), category=category)
239                         if created:
240                             tag_name = str(tag_name)
241                             tag.name = tag_name
242                             setattr(tag, "name_%s" % lang, tag_name)
243                             tag.sort_key = sortify(tag_sort_key.lower())
244                             tag.save()
245
246                         meta_tags.append(tag)
247                 else:
248                     meta_tags.append(tag)
249         return meta_tags
250
251
252 TagRelation.tag_model = Tag
253
254
255 def prefetch_relations(objects, category, only_name=True):
256     queryset = TagRelation.objects.filter(tag__category=category).select_related('tag')
257     if only_name:
258         queryset = queryset.only('tag__name_pl', 'object_id')
259     return objects.prefetch_related(
260         Prefetch('tag_relations', queryset=queryset, to_attr='%s_relations' % category))
261
262
263 def prefetched_relations(obj, category):
264     if hasattr(obj, '%s_relations' % category):
265         return getattr(obj, '%s_relations' % category)
266     else:
267         return None