simplify TagRelation
[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 TagBase
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     class Meta:
41         db_table = 'catalogue_tag_relation'
42         unique_together = (('tag', 'content_type', 'object_id'),)
43
44     def __unicode__(self):
45         try:
46             return u'%s [%s]' % (self.content_type.get_object_for_this_type(pk=self.object_id), self.tag)
47         except ObjectDoesNotExist:
48             return u'<deleted> [%s]' % self.tag
49
50
51 class Tag(TagBase):
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     user = models.ForeignKey(User, blank=True, null=True)
64     gazeta_link = models.CharField(blank=True, max_length=240)
65     culturepl_link = models.CharField(blank=True, max_length=240)
66     wiki_link = models.CharField(blank=True, max_length=240)
67
68     created_at = models.DateTimeField(_('creation date'), auto_now_add=True, db_index=True)
69     changed_at = models.DateTimeField(_('creation date'), auto_now=True, db_index=True)
70
71     after_change = Signal(providing_args=['instance', 'languages'])
72
73     intermediary_table_model = TagRelation
74
75     class UrlDeprecationWarning(DeprecationWarning):
76         def __init__(self, tags=None):
77             super(Tag.UrlDeprecationWarning, self).__init__()
78             self.tags = tags
79
80     categories_rev = {
81         'autor': 'author',
82         'epoka': 'epoch',
83         'rodzaj': 'kind',
84         'gatunek': 'genre',
85         'motyw': 'theme',
86         'polka': 'set',
87         'obiekt': 'thing',
88     }
89     categories_dict = dict((item[::-1] for item in categories_rev.iteritems()))
90
91     class Meta:
92         ordering = ('sort_key',)
93         verbose_name = _('tag')
94         verbose_name_plural = _('tags')
95         unique_together = (("slug", "category"),)
96         app_label = 'catalogue'
97
98     def save(self, *args, **kwargs):
99         flush_cache = flush_all_includes = False
100         if self.pk and self.category != 'set':
101             # Flush the whole views cache.
102             # Seem a little harsh, but changed tag names, descriptions
103             # and links come up at any number of places.
104             flush_cache = True
105
106             # Find in which languages we need to flush related includes.
107             old_self = type(self).objects.get(pk=self.pk)
108             # Category shouldn't normally be changed, but just in case.
109             if self.category != old_self.category:
110                 flush_all_includes = True
111             languages_changed = self.languages_changed(old_self)
112
113         ret = super(Tag, self).save(*args, **kwargs)
114
115         if flush_cache:
116             caches[settings.CACHE_MIDDLEWARE_ALIAS].clear()
117             if flush_all_includes:
118                 flush_ssi_includes()
119             else:
120                 self.flush_includes()
121             self.after_change.send(sender=type(self), instance=self, languages=languages_changed)
122
123         return ret
124
125     def languages_changed(self, old):
126         all_langs = [lc for (lc, _ln) in settings.LANGUAGES]
127         if (old.category, old.slug) != (self.category, self.slug):
128             return all_langs
129         languages = set()
130         for lang in all_langs:
131             name_field = 'name_%s' % lang
132             if getattr(old, name_field) != getattr(self, name_field):
133                 languages.add(lang)
134         return languages
135
136     def flush_includes(self, languages=True):
137         if not languages:
138             return
139         if languages is True:
140             languages = [lc for (lc, _ln) in settings.LANGUAGES]
141         flush_ssi_includes([
142             template % (self.pk, lang)
143             for template in [
144                 '/api/include/tag/%d.%s.json',
145                 '/api/include/tag/%d.%s.xml',
146                 ]
147             for lang in languages
148             ])
149         flush_ssi_includes([
150             '/katalog/%s.json' % lang for lang in languages])
151
152     def __unicode__(self):
153         return self.name
154
155     def __repr__(self):
156         return "Tag(slug=%r)" % self.slug
157
158     def get_initial(self):
159         if self.category == 'author':
160             return self.sort_key[0]
161         elif self.name:
162             return self.name[0]
163         else:
164             return ''
165
166     @permalink
167     def get_absolute_url(self):
168         return 'tagged_object_list', [self.url_chunk]
169
170     @permalink
171     def get_absolute_gallery_url(self):
172         return 'tagged_object_list_gallery', [self.url_chunk]
173
174     @classmethod
175     @permalink
176     def create_url(cls, category, slug):
177         return ('catalogue.views.tagged_object_list', [
178             '/'.join((cls.categories_dict[category], slug))
179         ])
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 isinstance(tag_str, basestring):
189             if not tag_str:
190                 return []
191             tags = []
192             ambiguous_slugs = []
193             category = None
194             deprecated = False
195             tags_splitted = tag_str.split('/')
196             for name in tags_splitted:
197                 if category:
198                     tags.append(Tag.objects.get(slug=name, category=category))
199                     category = None
200                 elif name in Tag.categories_rev:
201                     category = Tag.categories_rev[name]
202                 else:
203                     try:
204                         tags.append(Tag.objects.get(slug=name))
205                         deprecated = True
206                     except Tag.MultipleObjectsReturned:
207                         ambiguous_slugs.append(name)
208
209             if category:
210                 # something strange left off
211                 raise Tag.DoesNotExist()
212             if ambiguous_slugs:
213                 # some tags should be qualified
214                 e = Tag.MultipleObjectsReturned()
215                 e.tags = tags
216                 e.ambiguous_slugs = ambiguous_slugs
217                 raise e
218             if deprecated:
219                 raise Tag.UrlDeprecationWarning(tags=tags)
220             return tags
221         else:
222             return TagBase.get_tag_list(tag_str)
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 fnpdjango.utils.text.slughifi import slughifi
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 KeyError:
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=slughifi(tag_name), category=category)
252                     if created:
253                         tag_name = unicode(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 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