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