layout stuff
[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         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 = _('description')
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