Initial not None.
[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(_('category'), max_length=50, blank=False, null=False,
37         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: return []
163             real_tags = []
164             ambiguous_slugs = []
165             category = None
166             deprecated = False
167             tags_splitted = tags.split('/')
168             for name in tags_splitted:
169                 if category:
170                     real_tags.append(Tag.objects.get(slug=name, category=category))
171                     category = None
172                 elif name in Tag.categories_rev:
173                     category = Tag.categories_rev[name]
174                 else:
175                     try:
176                         real_tags.append(Tag.objects.get(slug=name))
177                         deprecated = True
178                     except Tag.MultipleObjectsReturned, e:
179                         ambiguous_slugs.append(name)
180
181             if category:
182                 # something strange left off
183                 raise Tag.DoesNotExist()
184             if ambiguous_slugs:
185                 # some tags should be qualified
186                 e = Tag.MultipleObjectsReturned()
187                 e.tags = real_tags
188                 e.ambiguous_slugs = ambiguous_slugs
189                 raise e
190             if deprecated:
191                 e = Tag.UrlDeprecationWarning()
192                 e.tags = real_tags
193                 raise e
194             return real_tags
195         else:
196             return TagBase.get_tag_list(tags)
197
198     @property
199     def url_chunk(self):
200         return '/'.join((Tag.categories_dict[self.category], self.slug))
201
202     @staticmethod
203     def tags_from_info(info):
204         from fnpdjango.utils.text.slughifi import slughifi
205         from sortify import sortify
206         meta_tags = []
207         categories = (('kinds', 'kind'), ('genres', 'genre'), ('authors', 'author'), ('epochs', 'epoch'))
208         for field_name, category in categories:
209             try:
210                 tag_names = getattr(info, field_name)
211             except:
212                 try:
213                     tag_names = [getattr(info, category)]
214                 except:
215                     # For instance, Pictures do not have 'genre' field.
216                     continue
217             for tag_name in tag_names:
218                 lang = getattr(tag_name, 'lang', settings.LANGUAGE_CODE)
219                 tag_sort_key = tag_name
220                 if category == 'author':
221                     tag_sort_key = tag_name.last_name
222                     tag_name = tag_name.readable()
223                 if lang == settings.LANGUAGE_CODE:
224                     # Allow creating new tag, if it's in default language.
225                     tag, created = Tag.objects.get_or_create(slug=slughifi(tag_name), category=category)
226                     if created:
227                         tag_name = unicode(tag_name)
228                         tag.name = tag_name
229                         setattr(tag, "name_%s" % lang, tag_name)
230                         tag.sort_key = sortify(tag_sort_key.lower())
231                         tag.save()
232
233                     meta_tags.append(tag)
234                 else:
235                     # Ignore unknown tags in non-default languages.
236                     try:
237                         tag = Tag.objects.get(category=category, **{"name_%s" % lang: tag_name})
238                     except Tag.DoesNotExist:
239                         pass
240                     else:
241                         meta_tags.append(tag)
242         return meta_tags
243
244
245 # Pickle complains about not having this.
246 TagRelation = Tag.intermediary_table_model