Code layout change.
[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     @permalink
132     def get_absolute_url(self):
133         return ('catalogue.views.tagged_object_list', [self.url_chunk])
134
135     @classmethod
136     @permalink
137     def create_url(cls, category, slug):
138         return ('catalogue.views.tagged_object_list', [
139                 '/'.join((cls.categories_dict[category], slug))
140             ])
141
142     def has_description(self):
143         return len(self.description) > 0
144     has_description.short_description = _('description')
145     has_description.boolean = True
146
147     @staticmethod
148     def get_tag_list(tags):
149         if isinstance(tags, basestring):
150             real_tags = []
151             ambiguous_slugs = []
152             category = None
153             deprecated = False
154             tags_splitted = tags.split('/')
155             for name in tags_splitted:
156                 if category:
157                     real_tags.append(Tag.objects.get(slug=name, category=category))
158                     category = None
159                 elif name in Tag.categories_rev:
160                     category = Tag.categories_rev[name]
161                 else:
162                     try:
163                         real_tags.append(Tag.objects.get(slug=name))
164                         deprecated = True
165                     except Tag.MultipleObjectsReturned, e:
166                         ambiguous_slugs.append(name)
167
168             if category:
169                 # something strange left off
170                 raise Tag.DoesNotExist()
171             if ambiguous_slugs:
172                 # some tags should be qualified
173                 e = Tag.MultipleObjectsReturned()
174                 e.tags = real_tags
175                 e.ambiguous_slugs = ambiguous_slugs
176                 raise e
177             if deprecated:
178                 e = Tag.UrlDeprecationWarning()
179                 e.tags = real_tags
180                 raise e
181             return real_tags
182         else:
183             return TagBase.get_tag_list(tags)
184
185     @property
186     def url_chunk(self):
187         return '/'.join((Tag.categories_dict[self.category], self.slug))
188
189     @staticmethod
190     def tags_from_info(info):
191         from fnpdjango.utils.text.slughifi import slughifi
192         from sortify import sortify
193         meta_tags = []
194         categories = (('kinds', 'kind'), ('genres', 'genre'), ('authors', 'author'), ('epochs', 'epoch'))
195         for field_name, category in categories:
196             try:
197                 tag_names = getattr(info, field_name)
198             except:
199                 try:
200                     tag_names = [getattr(info, category)]
201                 except:
202                     # For instance, Pictures do not have 'genre' field.
203                     continue
204             for tag_name in tag_names:
205                 lang = getattr(tag_name, 'lang', settings.LANGUAGE_CODE)
206                 tag_sort_key = tag_name
207                 if category == 'author':
208                     tag_sort_key = tag_name.last_name
209                     tag_name = tag_name.readable()
210                 if lang == settings.LANGUAGE_CODE:
211                     # Allow creating new tag, if it's in default language.
212                     tag, created = Tag.objects.get_or_create(slug=slughifi(tag_name), category=category)
213                     if created:
214                         tag_name = unicode(tag_name)
215                         tag.name = tag_name
216                         setattr(tag, "name_%s" % lang, tag_name)
217                         tag.sort_key = sortify(tag_sort_key.lower())
218                         tag.save()
219
220                     meta_tags.append(tag)
221                 else:
222                     # Ignore unknown tags in non-default languages.
223                     try:
224                         tag = Tag.objects.get(category=category, **{"name_%s" % lang: tag_name})
225                     except Tag.DoesNotExist:
226                         pass
227                     else:
228                         meta_tags.append(tag)
229         return meta_tags
230
231
232 # Pickle complains about not having this.
233 TagRelation = Tag.intermediary_table_model