Merge branch 'master' into obrazy
[wolnelektury.git] / apps / 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.auth.models import User
7 from django.core.exceptions import ValidationError
8 from django.db import models
9 from django.db.models import permalink
10 from django.utils.translation import ugettext, ugettext_lazy as _
11 from newtagging.models import TagBase
12
13
14 # Those are hard-coded here so that makemessages sees them.
15 TAG_CATEGORIES = (
16     ('author', _('author')),
17     ('epoch', _('epoch')),
18     ('kind', _('kind')),
19     ('genre', _('genre')),
20     ('theme', _('theme')),
21     ('set', _('set')),
22     ('book', _('book')),
23     ('thing', _('thing')), # things shown on pictures
24 )
25
26
27 class Tag(TagBase):
28     """A tag attachable to books and fragments (and possibly anything).
29     
30     Used to represent searchable metadata (authors, epochs, genres, kinds),
31     fragment themes (motifs) and some book hierarchy related kludges."""
32     name = models.CharField(_('name'), max_length=50, db_index=True)
33     slug = models.SlugField(_('slug'), max_length=120, db_index=True)
34     sort_key = models.CharField(_('sort key'), max_length=120, db_index=True)
35     category = models.CharField(_('category'), max_length=50, blank=False, null=False,
36         db_index=True, choices=TAG_CATEGORIES)
37     description = models.TextField(_('description'), blank=True)
38
39     user = models.ForeignKey(User, blank=True, null=True)
40     book_count = models.IntegerField(_('book count'), blank=True, null=True)
41     picture_count = models.IntegerField(_('picture count'), blank=True, null=True)
42     gazeta_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     class UrlDeprecationWarning(DeprecationWarning):
49         pass
50
51     categories_rev = {
52         'autor': 'author',
53         'epoka': 'epoch',
54         'rodzaj': 'kind',
55         'gatunek': 'genre',
56         'motyw': 'theme',
57         'polka': 'set',
58         'obiekt': 'thing',
59     }
60     categories_dict = dict((item[::-1] for item in categories_rev.iteritems()))
61
62     class Meta:
63         ordering = ('sort_key',)
64         verbose_name = _('tag')
65         verbose_name_plural = _('tags')
66         unique_together = (("slug", "category"),)
67         app_label = 'catalogue'
68
69     def __unicode__(self):
70         return self.name
71
72     def __repr__(self):
73         return "Tag(slug=%r)" % self.slug
74
75     @permalink
76     def get_absolute_url(self):
77         return ('catalogue.views.tagged_object_list', [self.url_chunk])
78
79     def clean(self):
80         if self.category == 'book' and (self.gazeta_link or self.wiki_link):
81             raise ValidationError(ugettext(
82                 u"Book tags can't have attached links. Set them directly on the book instead of it's tag."))
83
84     @classmethod
85     @permalink
86     def create_url(cls, category, slug):
87         return ('catalogue.views.tagged_object_list', [
88                 '/'.join((cls.categories_dict[category], slug))
89             ])
90
91     def has_description(self):
92         return len(self.description) > 0
93     has_description.short_description = _('description')
94     has_description.boolean = True
95
96     def get_count(self):
97         """Returns global book count for book tags, fragment count for themes."""
98         from catalogue.models import Book, Fragment
99
100         if self.category == 'book':
101             # never used
102             objects = Book.objects.none()
103         elif self.category == 'theme':
104             objects = Fragment.tagged.with_all((self,))
105         else:
106             objects = Book.tagged.with_all((self,)).order_by()
107             if self.category != 'set':
108                 # eliminate descendants
109                 l_tags = Tag.objects.filter(slug__in=[book.book_tag_slug() for book in objects.iterator()])
110                 descendants_keys = [book.pk for book in Book.tagged.with_any(l_tags).iterator()]
111                 if descendants_keys:
112                     objects = objects.exclude(pk__in=descendants_keys)
113         return objects.count()
114
115     # I shouldn't break the get_count() api 
116     # just to include pictures.
117     def get_picture_count(self):
118         from picture.models import Picture
119         
120         if self.category == 'book':
121             # never used
122             objects = Book.objects.none()
123         elif self.category == 'theme':
124             objects = Picture.tagged.with_all((self,))
125         elif self.category == 'thing':
126             objects = Picture.tagged.with_all((self,))
127         else:
128             objects = Picture.tagged.with_all((self,)).order_by()
129         return objects.count()
130         
131     @staticmethod
132     def get_tag_list(tags):
133         if isinstance(tags, basestring):
134             real_tags = []
135             ambiguous_slugs = []
136             category = None
137             deprecated = False
138             tags_splitted = tags.split('/')
139             for name in tags_splitted:
140                 if category:
141                     real_tags.append(Tag.objects.get(slug=name, category=category))
142                     category = None
143                 elif name in Tag.categories_rev:
144                     category = Tag.categories_rev[name]
145                 else:
146                     try:
147                         real_tags.append(Tag.objects.exclude(category='book').get(slug=name))
148                         deprecated = True 
149                     except Tag.MultipleObjectsReturned, e:
150                         ambiguous_slugs.append(name)
151
152             if category:
153                 # something strange left off
154                 raise Tag.DoesNotExist()
155             if ambiguous_slugs:
156                 # some tags should be qualified
157                 e = Tag.MultipleObjectsReturned()
158                 e.tags = real_tags
159                 e.ambiguous_slugs = ambiguous_slugs
160                 raise e
161             if deprecated:
162                 e = Tag.UrlDeprecationWarning()
163                 e.tags = real_tags
164                 raise e
165             return real_tags
166         else:
167             return TagBase.get_tag_list(tags)
168
169     @property
170     def url_chunk(self):
171         return '/'.join((Tag.categories_dict[self.category], self.slug))
172
173     @staticmethod
174     def tags_from_info(info):
175         from fnpdjango.utils.text.slughifi import slughifi
176         from sortify import sortify
177         meta_tags = []
178         categories = (('kinds', 'kind'), ('genres', 'genre'), ('authors', 'author'), ('epochs', 'epoch'))
179         for field_name, category in categories:
180             try:
181                 tag_names = getattr(info, field_name)
182             except:
183                 try:
184                     tag_names = [getattr(info, category)]
185                 except:
186                     # For instance, Pictures do not have 'genre' field.
187                     continue
188             for tag_name in tag_names:
189                 lang = getattr(tag_name, 'lang', settings.LANGUAGE_CODE)
190                 tag_sort_key = tag_name
191                 if category == 'author':
192                     tag_sort_key = tag_name.last_name
193                     tag_name = tag_name.readable()
194                 if lang == settings.LANGUAGE_CODE:
195                     # Allow creating new tag, if it's in default language.
196                     tag, created = Tag.objects.get_or_create(slug=slughifi(tag_name), category=category)
197                     if created:
198                         tag.name = tag_name
199                         setattr(tag, "name_%s" % lang, tag_name)
200                         tag.sort_key = sortify(tag_sort_key.lower())
201                         tag.save()
202                     meta_tags.append(tag)
203                 else:
204                     # Ignore unknown tags in non-default languages.
205                     try:
206                         tag = Tag.objects.get(category=category, **{"name_%s" % lang: tag_name})
207                     except Tag.DoesNotExist:
208                         pass
209                     else:
210                         meta_tags.append(tag)
211         return meta_tags