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