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