2ade345a5de3fe5e014d86491855266901b43667
[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         return reverse(f'{self.category}_catalogue')
163
164     def has_description(self):
165         return len(self.description) > 0
166     has_description.short_description = _('description')
167     has_description.boolean = True
168
169     @staticmethod
170     def get_tag_list(tag_str):
171         if not tag_str:
172             return []
173         tags = []
174         ambiguous_slugs = []
175         category = None
176         deprecated = False
177         tags_splitted = tag_str.split('/')
178         for name in tags_splitted:
179             if category:
180                 tags.append(Tag.objects.get(slug=name, category=category))
181                 category = None
182             elif name in Tag.categories_rev:
183                 category = Tag.categories_rev[name]
184             else:
185                 try:
186                     tags.append(Tag.objects.get(slug=name))
187                     deprecated = True
188                 except Tag.MultipleObjectsReturned:
189                     ambiguous_slugs.append(name)
190
191         if category:
192             # something strange left off
193             raise Tag.DoesNotExist()
194         if ambiguous_slugs:
195             # some tags should be qualified
196             e = Tag.MultipleObjectsReturned()
197             e.tags = tags
198             e.ambiguous_slugs = ambiguous_slugs
199             raise e
200         if deprecated:
201             raise Tag.UrlDeprecationWarning(tags=tags)
202         return tags
203
204     @property
205     def url_chunk(self):
206         return '/'.join((Tag.categories_dict[self.category], self.slug))
207
208     @staticmethod
209     def tags_from_info(info):
210         from slugify import slugify
211         from sortify import sortify
212         meta_tags = []
213         categories = (('kinds', 'kind'), ('genres', 'genre'), ('authors', 'author'), ('epochs', 'epoch'))
214         for field_name, category in categories:
215             try:
216                 tag_names = getattr(info, field_name)
217             except (AttributeError, KeyError):  # TODO: shouldn't be KeyError here at all.
218                 try:
219                     tag_names = [getattr(info, category)]
220                 except KeyError:
221                     # For instance, Pictures do not have 'genre' field.
222                     continue
223             for tag_name in tag_names:
224                 lang = getattr(tag_name, 'lang', None) or settings.LANGUAGE_CODE
225                 tag_sort_key = tag_name
226                 if category == 'author':
227                     tag_sort_key = ' '.join((tag_name.last_name,) + tag_name.first_names)
228                     tag_name = tag_name.readable()
229
230                 try:
231                     tag = Tag.objects.get(category=category, **{"name_%s" % lang: tag_name})
232                 except Tag.DoesNotExist:
233                     if lang == settings.LANGUAGE_CODE:
234                         # Allow creating new tag, if it's in default language.
235                         tag, created = Tag.objects.get_or_create(slug=slugify(tag_name), category=category)
236                         if created:
237                             tag_name = str(tag_name)
238                             tag.name = tag_name
239                             setattr(tag, "name_%s" % lang, tag_name)
240                             tag.sort_key = sortify(tag_sort_key.lower())
241                             tag.save()
242
243                         meta_tags.append(tag)
244                 else:
245                     meta_tags.append(tag)
246         return meta_tags
247
248
249 TagRelation.tag_model = Tag
250
251
252 def prefetch_relations(objects, category, only_name=True):
253     queryset = TagRelation.objects.filter(tag__category=category).select_related('tag')
254     if only_name:
255         queryset = queryset.only('tag__name_pl', 'object_id')
256     return objects.prefetch_related(
257         Prefetch('tag_relations', queryset=queryset, to_attr='%s_relations' % category))
258
259
260 def prefetched_relations(obj, category):
261     if hasattr(obj, '%s_relations' % category):
262         return getattr(obj, '%s_relations' % category)
263     else:
264         return None