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