740b84bacb3632b1dbbd9a475e8fca96f21d4ee9
[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     after_change = Signal()
77
78     intermediary_table_model = TagRelation
79     objects = TagManager()
80
81     class UrlDeprecationWarning(DeprecationWarning):
82         def __init__(self, tags=None):
83             super(Tag.UrlDeprecationWarning, self).__init__()
84             self.tags = tags
85
86     categories_rev = {
87         'autor': 'author',
88         'epoka': 'epoch',
89         'rodzaj': 'kind',
90         'gatunek': 'genre',
91         'motyw': 'theme',
92         'polka': 'set',
93         'obiekt': 'thing',
94     }
95     categories_dict = dict((item[::-1] for item in categories_rev.items()))
96
97     class Meta:
98         ordering = ('sort_key',)
99         verbose_name = _('tag')
100         verbose_name_plural = _('tags')
101         unique_together = (("slug", "category"),)
102         app_label = 'catalogue'
103
104     def save(self, *args, quick=False, **kwargs):
105         existing = self.pk and self.category != 'set'
106         ret = super(Tag, self).save(*args, **kwargs)
107         if existing and not quick:
108             self.after_change.send(sender=type(self), instance=self)
109         return ret
110
111     def __str__(self):
112         return self.name
113
114     def __repr__(self):
115         return "Tag(slug=%r)" % self.slug
116
117     def get_initial(self):
118         if self.category == 'author':
119             return self.sort_key[0]
120         elif self.name:
121             return self.name[0]
122         else:
123             return ''
124
125     @property
126     def category_plural(self):
127         return self.category + 's'
128
129     def get_absolute_url(self):
130         return reverse('tagged_object_list', args=[self.url_chunk])
131
132     def get_absolute_gallery_url(self):
133         return reverse('tagged_object_list_gallery', args=[self.url_chunk])
134
135     def has_description(self):
136         return len(self.description) > 0
137     has_description.short_description = _('description')
138     has_description.boolean = True
139
140     @staticmethod
141     def get_tag_list(tag_str):
142         if not tag_str:
143             return []
144         tags = []
145         ambiguous_slugs = []
146         category = None
147         deprecated = False
148         tags_splitted = tag_str.split('/')
149         for name in tags_splitted:
150             if category:
151                 tags.append(Tag.objects.get(slug=name, category=category))
152                 category = None
153             elif name in Tag.categories_rev:
154                 category = Tag.categories_rev[name]
155             else:
156                 try:
157                     tags.append(Tag.objects.get(slug=name))
158                     deprecated = True
159                 except Tag.MultipleObjectsReturned:
160                     ambiguous_slugs.append(name)
161
162         if category:
163             # something strange left off
164             raise Tag.DoesNotExist()
165         if ambiguous_slugs:
166             # some tags should be qualified
167             e = Tag.MultipleObjectsReturned()
168             e.tags = tags
169             e.ambiguous_slugs = ambiguous_slugs
170             raise e
171         if deprecated:
172             raise Tag.UrlDeprecationWarning(tags=tags)
173         return tags
174
175     @property
176     def url_chunk(self):
177         return '/'.join((Tag.categories_dict[self.category], self.slug))
178
179     @staticmethod
180     def tags_from_info(info):
181         from slugify import slugify
182         from sortify import sortify
183         meta_tags = []
184         categories = (('kinds', 'kind'), ('genres', 'genre'), ('authors', 'author'), ('epochs', 'epoch'))
185         for field_name, category in categories:
186             try:
187                 tag_names = getattr(info, field_name)
188             except (AttributeError, KeyError):  # TODO: shouldn't be KeyError here at all.
189                 try:
190                     tag_names = [getattr(info, category)]
191                 except KeyError:
192                     # For instance, Pictures do not have 'genre' field.
193                     continue
194             for tag_name in tag_names:
195                 lang = getattr(tag_name, 'lang', None) or settings.LANGUAGE_CODE
196                 tag_sort_key = tag_name
197                 if category == 'author':
198                     tag_sort_key = ' '.join((tag_name.last_name,) + tag_name.first_names)
199                     tag_name = tag_name.readable()
200                 if lang == settings.LANGUAGE_CODE:
201                     # Allow creating new tag, if it's in default language.
202                     tag, created = Tag.objects.get_or_create(slug=slugify(tag_name), category=category)
203                     if created:
204                         tag_name = str(tag_name)
205                         tag.name = tag_name
206                         setattr(tag, "name_%s" % lang, tag_name)
207                         tag.sort_key = sortify(tag_sort_key.lower())
208                         tag.save()
209
210                     meta_tags.append(tag)
211                 else:
212                     # Ignore unknown tags in non-default languages.
213                     try:
214                         tag = Tag.objects.get(category=category, **{"name_%s" % lang: tag_name})
215                     except Tag.DoesNotExist:
216                         pass
217                     else:
218                         meta_tags.append(tag)
219         return meta_tags
220
221
222 TagRelation.tag_model = Tag
223
224
225 def prefetch_relations(objects, category, only_name=True):
226     queryset = TagRelation.objects.filter(tag__category=category).select_related('tag')
227     if only_name:
228         queryset = queryset.only('tag__name_pl', 'object_id')
229     return objects.prefetch_related(
230         Prefetch('tag_relations', queryset=queryset, to_attr='%s_relations' % category))
231
232
233 def prefetched_relations(obj, category):
234     if hasattr(obj, '%s_relations' % category):
235         return getattr(obj, '%s_relations' % category)
236     else:
237         return None