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