Introduce DRF and start replacing the views.
[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     @property
170     def category_plural(self):
171         return self.category + 's'
172
173     @permalink
174     def get_absolute_url(self):
175         return 'tagged_object_list', [self.url_chunk]
176
177     @permalink
178     def get_absolute_gallery_url(self):
179         return 'tagged_object_list_gallery', [self.url_chunk]
180
181     @classmethod
182     @permalink
183     def create_url(cls, category, slug):
184         return ('catalogue.views.tagged_object_list', [
185             '/'.join((cls.categories_dict[category], slug))
186         ])
187
188     def has_description(self):
189         return len(self.description) > 0
190     has_description.short_description = _('description')
191     has_description.boolean = True
192
193     @staticmethod
194     def get_tag_list(tag_str):
195         if isinstance(tag_str, basestring):
196             if not tag_str:
197                 return []
198             tags = []
199             ambiguous_slugs = []
200             category = None
201             deprecated = False
202             tags_splitted = tag_str.split('/')
203             for name in tags_splitted:
204                 if category:
205                     tags.append(Tag.objects.get(slug=name, category=category))
206                     category = None
207                 elif name in Tag.categories_rev:
208                     category = Tag.categories_rev[name]
209                 else:
210                     try:
211                         tags.append(Tag.objects.get(slug=name))
212                         deprecated = True
213                     except Tag.MultipleObjectsReturned:
214                         ambiguous_slugs.append(name)
215
216             if category:
217                 # something strange left off
218                 raise Tag.DoesNotExist()
219             if ambiguous_slugs:
220                 # some tags should be qualified
221                 e = Tag.MultipleObjectsReturned()
222                 e.tags = tags
223                 e.ambiguous_slugs = ambiguous_slugs
224                 raise e
225             if deprecated:
226                 raise Tag.UrlDeprecationWarning(tags=tags)
227             return tags
228         else:
229             return TagBase.get_tag_list(tag_str)
230
231     @property
232     def url_chunk(self):
233         return '/'.join((Tag.categories_dict[self.category], self.slug))
234
235     @staticmethod
236     def tags_from_info(info):
237         from slugify import slugify
238         from sortify import sortify
239         meta_tags = []
240         categories = (('kinds', 'kind'), ('genres', 'genre'), ('authors', 'author'), ('epochs', 'epoch'))
241         for field_name, category in categories:
242             try:
243                 tag_names = getattr(info, field_name)
244             except KeyError:
245                 try:
246                     tag_names = [getattr(info, category)]
247                 except KeyError:
248                     # For instance, Pictures do not have 'genre' field.
249                     continue
250             for tag_name in tag_names:
251                 lang = getattr(tag_name, 'lang', settings.LANGUAGE_CODE)
252                 tag_sort_key = tag_name
253                 if category == 'author':
254                     tag_sort_key = ' '.join((tag_name.last_name,) + tag_name.first_names)
255                     tag_name = tag_name.readable()
256                 if lang == settings.LANGUAGE_CODE:
257                     # Allow creating new tag, if it's in default language.
258                     tag, created = Tag.objects.get_or_create(slug=slugify(tag_name), category=category)
259                     if created:
260                         tag_name = unicode(tag_name)
261                         tag.name = tag_name
262                         setattr(tag, "name_%s" % lang, tag_name)
263                         tag.sort_key = sortify(tag_sort_key.lower())
264                         tag.save()
265
266                     meta_tags.append(tag)
267                 else:
268                     # Ignore unknown tags in non-default languages.
269                     try:
270                         tag = Tag.objects.get(category=category, **{"name_%s" % lang: tag_name})
271                     except Tag.DoesNotExist:
272                         pass
273                     else:
274                         meta_tags.append(tag)
275         return meta_tags
276
277
278 def prefetch_relations(objects, category, only_name=True):
279     queryset = TagRelation.objects.filter(tag__category=category).select_related('tag')
280     if only_name:
281         queryset = queryset.only('tag__name_pl', 'object_id')
282     return objects.prefetch_related(
283         Prefetch('tag_relations', queryset=queryset, to_attr='%s_relations' % category))
284
285
286 def prefetched_relations(obj, category):
287     if hasattr(obj, '%s_relations' % category):
288         return getattr(obj, '%s_relations' % category)
289     else:
290         return None