remove the banner
[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 = (
217             # BookInfo field names, Tag category, relationship
218             ('kinds', 'kind', None),
219             ('genres', 'genre', None),
220             ('epochs', 'epoch', None),
221             ('authors', 'author', None),
222             ('translators', 'author', 'translator'),
223         )
224         for field_name, category, relationship in categories:
225             try:
226                 tag_names = getattr(info, field_name)
227             except (AttributeError, KeyError):  # TODO: shouldn't be KeyError here at all.
228                 # For instance, Pictures do not have 'genre' field.
229                 continue
230             for tag_name in tag_names:
231                 lang = getattr(tag_name, 'lang', None) or settings.LANGUAGE_CODE
232                 tag_sort_key = tag_name
233                 if category == 'author':
234                     tag_sort_key = ' '.join((tag_name.last_name,) + tag_name.first_names)
235                     tag_name = tag_name.readable()
236
237                 try:
238                     tag = Tag.objects.get(category=category, **{"name_%s" % lang: tag_name})
239                 except Tag.DoesNotExist:
240                     if lang == settings.LANGUAGE_CODE:
241                         # Allow creating new tag, if it's in default language.
242                         tag, created = Tag.objects.get_or_create(slug=slugify(tag_name), category=category)
243                         if created:
244                             tag_name = str(tag_name)
245                             tag.name = tag_name
246                             setattr(tag, "name_%s" % lang, tag_name)
247                             tag.sort_key = sortify(tag_sort_key.lower())
248                             tag.save()
249
250                         meta_tags.append((tag, relationship))
251                 else:
252                     meta_tags.append((tag, relationship))
253         return meta_tags
254
255
256 TagRelation.tag_model = Tag
257
258
259 def prefetch_relations(objects, category, only_name=True):
260     queryset = TagRelation.objects.filter(tag__category=category).select_related('tag')
261     if only_name:
262         queryset = queryset.only('tag__name_pl', 'object_id')
263     return objects.prefetch_related(
264         Prefetch('tag_relations', queryset=queryset, to_attr='%s_relations' % category))
265
266
267 def prefetched_relations(obj, category):
268     if hasattr(obj, '%s_relations' % category):
269         return getattr(obj, '%s_relations' % category)
270     else:
271         return None