book/picture-only tags
[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     for_books = models.BooleanField(default=False)
64     for_pictures = models.BooleanField(default=False)
65
66     user = models.ForeignKey(User, blank=True, null=True)
67     gazeta_link = models.CharField(blank=True, max_length=240)
68     culturepl_link = models.CharField(blank=True, max_length=240)
69     wiki_link = models.CharField(blank=True, max_length=240)
70
71     created_at = models.DateTimeField(_('creation date'), auto_now_add=True, db_index=True)
72     changed_at = models.DateTimeField(_('creation date'), auto_now=True, db_index=True)
73
74     after_change = Signal(providing_args=['instance', 'languages'])
75
76     intermediary_table_model = TagRelation
77
78     class UrlDeprecationWarning(DeprecationWarning):
79         def __init__(self, tags=None):
80             super(Tag.UrlDeprecationWarning, self).__init__()
81             self.tags = tags
82
83     categories_rev = {
84         'autor': 'author',
85         'epoka': 'epoch',
86         'rodzaj': 'kind',
87         'gatunek': 'genre',
88         'motyw': 'theme',
89         'polka': 'set',
90         'obiekt': 'thing',
91     }
92     categories_dict = dict((item[::-1] for item in categories_rev.iteritems()))
93
94     class Meta:
95         ordering = ('sort_key',)
96         verbose_name = _('tag')
97         verbose_name_plural = _('tags')
98         unique_together = (("slug", "category"),)
99         app_label = 'catalogue'
100
101     def save(self, *args, **kwargs):
102         flush_cache = flush_all_includes = False
103         if self.pk and self.category != 'set':
104             # Flush the whole views cache.
105             # Seem a little harsh, but changed tag names, descriptions
106             # and links come up at any number of places.
107             flush_cache = True
108
109             # Find in which languages we need to flush related includes.
110             old_self = type(self).objects.get(pk=self.pk)
111             # Category shouldn't normally be changed, but just in case.
112             if self.category != old_self.category:
113                 flush_all_includes = True
114             languages_changed = self.languages_changed(old_self)
115
116         ret = super(Tag, self).save(*args, **kwargs)
117
118         if flush_cache:
119             caches[settings.CACHE_MIDDLEWARE_ALIAS].clear()
120             if flush_all_includes:
121                 flush_ssi_includes()
122             else:
123                 self.flush_includes()
124             self.after_change.send(sender=type(self), instance=self, languages=languages_changed)
125
126         return ret
127
128     def languages_changed(self, old):
129         all_langs = [lc for (lc, _ln) in settings.LANGUAGES]
130         if (old.category, old.slug) != (self.category, self.slug):
131             return all_langs
132         languages = set()
133         for lang in all_langs:
134             name_field = 'name_%s' % lang
135             if getattr(old, name_field) != getattr(self, name_field):
136                 languages.add(lang)
137         return languages
138
139     def flush_includes(self, languages=True):
140         if not languages:
141             return
142         if languages is True:
143             languages = [lc for (lc, _ln) in settings.LANGUAGES]
144         flush_ssi_includes([
145             template % (self.pk, lang)
146             for template in [
147                 '/api/include/tag/%d.%s.json',
148                 '/api/include/tag/%d.%s.xml',
149                 ]
150             for lang in languages
151             ])
152         flush_ssi_includes([
153             '/katalog/%s.json' % lang for lang in languages])
154
155     def __unicode__(self):
156         return self.name
157
158     def __repr__(self):
159         return "Tag(slug=%r)" % self.slug
160
161     def get_initial(self):
162         if self.category == 'author':
163             return self.sort_key[0]
164         elif self.name:
165             return self.name[0]
166         else:
167             return ''
168
169     @permalink
170     def get_absolute_url(self):
171         return 'tagged_object_list', [self.url_chunk]
172
173     @permalink
174     def get_absolute_gallery_url(self):
175         return 'tagged_object_list_gallery', [self.url_chunk]
176
177     @classmethod
178     @permalink
179     def create_url(cls, category, slug):
180         return ('catalogue.views.tagged_object_list', [
181             '/'.join((cls.categories_dict[category], slug))
182         ])
183
184     def has_description(self):
185         return len(self.description) > 0
186     has_description.short_description = _('description')
187     has_description.boolean = True
188
189     @staticmethod
190     def get_tag_list(tag_str):
191         if isinstance(tag_str, basestring):
192             if not tag_str:
193                 return []
194             tags = []
195             ambiguous_slugs = []
196             category = None
197             deprecated = False
198             tags_splitted = tag_str.split('/')
199             for name in tags_splitted:
200                 if category:
201                     tags.append(Tag.objects.get(slug=name, category=category))
202                     category = None
203                 elif name in Tag.categories_rev:
204                     category = Tag.categories_rev[name]
205                 else:
206                     try:
207                         tags.append(Tag.objects.get(slug=name))
208                         deprecated = True
209                     except Tag.MultipleObjectsReturned:
210                         ambiguous_slugs.append(name)
211
212             if category:
213                 # something strange left off
214                 raise Tag.DoesNotExist()
215             if ambiguous_slugs:
216                 # some tags should be qualified
217                 e = Tag.MultipleObjectsReturned()
218                 e.tags = tags
219                 e.ambiguous_slugs = ambiguous_slugs
220                 raise e
221             if deprecated:
222                 raise Tag.UrlDeprecationWarning(tags=tags)
223             return tags
224         else:
225             return TagBase.get_tag_list(tag_str)
226
227     @property
228     def url_chunk(self):
229         return '/'.join((Tag.categories_dict[self.category], self.slug))
230
231     @staticmethod
232     def tags_from_info(info):
233         from fnpdjango.utils.text.slughifi import slughifi
234         from sortify import sortify
235         meta_tags = []
236         categories = (('kinds', 'kind'), ('genres', 'genre'), ('authors', 'author'), ('epochs', 'epoch'))
237         for field_name, category in categories:
238             try:
239                 tag_names = getattr(info, field_name)
240             except KeyError:
241                 try:
242                     tag_names = [getattr(info, category)]
243                 except KeyError:
244                     # For instance, Pictures do not have 'genre' field.
245                     continue
246             for tag_name in tag_names:
247                 lang = getattr(tag_name, 'lang', settings.LANGUAGE_CODE)
248                 tag_sort_key = tag_name
249                 if category == 'author':
250                     tag_sort_key = ' '.join((tag_name.last_name,) + tag_name.first_names)
251                     tag_name = tag_name.readable()
252                 if lang == settings.LANGUAGE_CODE:
253                     # Allow creating new tag, if it's in default language.
254                     tag, created = Tag.objects.get_or_create(slug=slughifi(tag_name), category=category)
255                     if created:
256                         tag_name = unicode(tag_name)
257                         tag.name = tag_name
258                         setattr(tag, "name_%s" % lang, tag_name)
259                         tag.sort_key = sortify(tag_sort_key.lower())
260                         tag.save()
261
262                     meta_tags.append(tag)
263                 else:
264                     # Ignore unknown tags in non-default languages.
265                     try:
266                         tag = Tag.objects.get(category=category, **{"name_%s" % lang: tag_name})
267                     except Tag.DoesNotExist:
268                         pass
269                     else:
270                         meta_tags.append(tag)
271         return meta_tags
272
273
274 def prefetch_relations(objects, category, only_name=True):
275     queryset = TagRelation.objects.filter(tag__category=category).select_related('tag')
276     if only_name:
277         queryset = queryset.only('tag__name_pl', 'object_id')
278     return objects.prefetch_related(
279         Prefetch('tag_relations', queryset=queryset, to_attr='%s_relations' % category))
280
281
282 def prefetched_relations(obj, category):
283     if hasattr(obj, '%s_relations' % category):
284         return getattr(obj, '%s_relations' % category)
285     else:
286         return None