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