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