Python 3
[wolnelektury.git] / src / catalogue / models / tag.py
1 # -*- coding: utf-8 -*-
2 # This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later.
3 # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
4 #
5 from django.conf import settings
6 from django.contrib.contenttypes.fields import GenericForeignKey
7 from django.contrib.contenttypes.models import ContentType
8 from django.core.cache import caches
9 from django.contrib.auth.models import User
10 from django.core.exceptions import ObjectDoesNotExist
11 from django.db import models
12 from django.db.models import permalink
13 from django.db.models.query import Prefetch
14 from django.dispatch import Signal
15 from django.utils.translation import ugettext_lazy as _
16
17 from newtagging.models import TagManager, TaggedItemManager
18 from ssify import flush_ssi_includes
19
20
21 # Those are hard-coded here so that makemessages sees them.
22 TAG_CATEGORIES = (
23     ('author', _('author')),
24     ('epoch', _('epoch')),
25     ('kind', _('kind')),
26     ('genre', _('genre')),
27     ('theme', _('theme')),
28     ('set', _('set')),
29     ('thing', _('thing')),  # things shown on pictures
30 )
31
32
33 class TagRelation(models.Model):
34
35     tag = models.ForeignKey('Tag', verbose_name=_('tag'), related_name='items')
36     content_type = models.ForeignKey(ContentType, verbose_name=_('content type'))
37     object_id = models.PositiveIntegerField(_('object id'), db_index=True)
38     content_object = GenericForeignKey('content_type', 'object_id')
39
40     objects = TaggedItemManager()
41
42     class Meta:
43         db_table = 'catalogue_tag_relation'
44         unique_together = (('tag', 'content_type', 'object_id'),)
45
46     def __str__(self):
47         try:
48             return u'%s [%s]' % (self.content_type.get_object_for_this_type(pk=self.object_id), self.tag)
49         except ObjectDoesNotExist:
50             return u'<deleted> [%s]' % self.tag
51
52
53 class Tag(models.Model):
54     """A tag attachable to books and fragments (and possibly anything).
55
56     Used to represent searchable metadata (authors, epochs, genres, kinds),
57     fragment themes (motifs) and some book hierarchy related kludges."""
58     name = models.CharField(_('name'), max_length=120, db_index=True)
59     slug = models.SlugField(_('slug'), max_length=120, db_index=True)
60     sort_key = models.CharField(_('sort key'), max_length=120, db_index=True)
61     category = models.CharField(
62         _('category'), max_length=50, blank=False, null=False, db_index=True, choices=TAG_CATEGORIES)
63     description = models.TextField(_('description'), blank=True)
64
65     for_books = models.BooleanField(default=False)
66     for_pictures = models.BooleanField(default=False)
67
68     user = models.ForeignKey(User, blank=True, null=True)
69     gazeta_link = models.CharField(blank=True, max_length=240)
70     culturepl_link = models.CharField(blank=True, max_length=240)
71     wiki_link = models.CharField(blank=True, max_length=240)
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(providing_args=['instance', 'languages'])
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, **kwargs):
105         flush_cache = flush_all_includes = False
106         if self.pk and self.category != 'set':
107             # Flush the whole views cache.
108             # Seem a little harsh, but changed tag names, descriptions
109             # and links come up at any number of places.
110             flush_cache = True
111
112             # Find in which languages we need to flush related includes.
113             old_self = type(self).objects.get(pk=self.pk)
114             # Category shouldn't normally be changed, but just in case.
115             if self.category != old_self.category:
116                 flush_all_includes = True
117             languages_changed = self.languages_changed(old_self)
118
119         ret = super(Tag, self).save(*args, **kwargs)
120
121         if flush_cache:
122             caches[settings.CACHE_MIDDLEWARE_ALIAS].clear()
123             if flush_all_includes:
124                 flush_ssi_includes()
125             else:
126                 self.flush_includes()
127             self.after_change.send(sender=type(self), instance=self, languages=languages_changed)
128
129         return ret
130
131     def languages_changed(self, old):
132         all_langs = [lc for (lc, _ln) in settings.LANGUAGES]
133         if (old.category, old.slug) != (self.category, self.slug):
134             return all_langs
135         languages = set()
136         for lang in all_langs:
137             name_field = 'name_%s' % lang
138             if getattr(old, name_field) != getattr(self, name_field):
139                 languages.add(lang)
140         return languages
141
142     def flush_includes(self, languages=True):
143         if not languages:
144             return
145         if languages is True:
146             languages = [lc for (lc, _ln) in settings.LANGUAGES]
147         flush_ssi_includes([
148             template % (self.pk, lang)
149             for template in [
150                 '/api/include/tag/%d.%s.json',
151                 '/api/include/tag/%d.%s.xml',
152                 ]
153             for lang in languages
154             ])
155         flush_ssi_includes([
156             '/katalog/%s.json' % lang for lang in languages])
157
158     def __str__(self):
159         return self.name
160
161     def __repr__(self):
162         return "Tag(slug=%r)" % self.slug
163
164     def get_initial(self):
165         if self.category == 'author':
166             return self.sort_key[0]
167         elif self.name:
168             return self.name[0]
169         else:
170             return ''
171
172     @property
173     def category_plural(self):
174         return self.category + 's'
175
176     @permalink
177     def get_absolute_url(self):
178         return 'tagged_object_list', [self.url_chunk]
179
180     @permalink
181     def get_absolute_gallery_url(self):
182         return 'tagged_object_list_gallery', [self.url_chunk]
183
184     def has_description(self):
185         return len(self.description) > 0
186     has_description.short_description = _('description')
187     has_description.boolean = True
188
189     @staticmethod
190     def get_tag_list(tag_str):
191         if not tag_str:
192             return []
193         tags = []
194         ambiguous_slugs = []
195         category = None
196         deprecated = False
197         tags_splitted = tag_str.split('/')
198         for name in tags_splitted:
199             if category:
200                 tags.append(Tag.objects.get(slug=name, category=category))
201                 category = None
202             elif name in Tag.categories_rev:
203                 category = Tag.categories_rev[name]
204             else:
205                 try:
206                     tags.append(Tag.objects.get(slug=name))
207                     deprecated = True
208                 except Tag.MultipleObjectsReturned:
209                     ambiguous_slugs.append(name)
210
211         if category:
212             # something strange left off
213             raise Tag.DoesNotExist()
214         if ambiguous_slugs:
215             # some tags should be qualified
216             e = Tag.MultipleObjectsReturned()
217             e.tags = tags
218             e.ambiguous_slugs = ambiguous_slugs
219             raise e
220         if deprecated:
221             raise Tag.UrlDeprecationWarning(tags=tags)
222         return tags
223
224     @property
225     def url_chunk(self):
226         return '/'.join((Tag.categories_dict[self.category], self.slug))
227
228     @staticmethod
229     def tags_from_info(info):
230         from slugify import slugify
231         from sortify import sortify
232         meta_tags = []
233         categories = (('kinds', 'kind'), ('genres', 'genre'), ('authors', 'author'), ('epochs', 'epoch'))
234         for field_name, category in categories:
235             try:
236                 tag_names = getattr(info, field_name)
237             except (AttributeError, KeyError):  # TODO: shouldn't be KeyError here at all.
238                 try:
239                     tag_names = [getattr(info, category)]
240                 except KeyError:
241                     # For instance, Pictures do not have 'genre' field.
242                     continue
243             for tag_name in tag_names:
244                 lang = getattr(tag_name, 'lang', settings.LANGUAGE_CODE)
245                 tag_sort_key = tag_name
246                 if category == 'author':
247                     tag_sort_key = ' '.join((tag_name.last_name,) + tag_name.first_names)
248                     tag_name = tag_name.readable()
249                 if lang == settings.LANGUAGE_CODE:
250                     # Allow creating new tag, if it's in default language.
251                     tag, created = Tag.objects.get_or_create(slug=slugify(tag_name), category=category)
252                     if created:
253                         tag_name = str(tag_name)
254                         tag.name = tag_name
255                         setattr(tag, "name_%s" % lang, tag_name)
256                         tag.sort_key = sortify(tag_sort_key.lower())
257                         tag.save()
258
259                     meta_tags.append(tag)
260                 else:
261                     # Ignore unknown tags in non-default languages.
262                     try:
263                         tag = Tag.objects.get(category=category, **{"name_%s" % lang: tag_name})
264                     except Tag.DoesNotExist:
265                         pass
266                     else:
267                         meta_tags.append(tag)
268         return meta_tags
269
270
271 TagRelation.tag_model = Tag
272
273
274 def prefetch_relations(objects, category, only_name=True):
275     queryset = TagRelation.objects.filter(tag__category=category).select_related('tag')
276     if only_name:
277         queryset = queryset.only('tag__name_pl', 'object_id')
278     return objects.prefetch_related(
279         Prefetch('tag_relations', queryset=queryset, to_attr='%s_relations' % category))
280
281
282 def prefetched_relations(obj, category):
283     if hasattr(obj, '%s_relations' % category):
284         return getattr(obj, '%s_relations' % category)
285     else:
286         return None