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