1 # This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later.
2 # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
4 from django.conf import settings
5 from django.contrib.contenttypes.fields import GenericForeignKey
6 from django.contrib.contenttypes.models import ContentType
7 from django.core.cache import caches
8 from django.contrib.auth.models import User
9 from django.core.exceptions import ObjectDoesNotExist
10 from django.db import models
11 from django.db.models.query import Prefetch
12 from django.dispatch import Signal
13 from django.urls import reverse
14 from django.utils.translation import ugettext_lazy as _
16 from newtagging.models import TagManager, TaggedItemManager
17 from ssify import flush_ssi_includes
20 # Those are hard-coded here so that makemessages sees them.
22 ('author', _('author')),
23 ('epoch', _('epoch')),
25 ('genre', _('genre')),
26 ('theme', _('theme')),
28 ('thing', _('thing')), # things shown on pictures
32 class TagRelation(models.Model):
34 tag = models.ForeignKey('Tag', models.CASCADE, verbose_name=_('tag'), related_name='items')
35 content_type = models.ForeignKey(ContentType, models.CASCADE, verbose_name=_('content type'))
36 object_id = models.PositiveIntegerField(_('object id'), db_index=True)
37 content_object = GenericForeignKey('content_type', 'object_id')
39 objects = TaggedItemManager()
42 db_table = 'catalogue_tag_relation'
43 unique_together = (('tag', 'content_type', 'object_id'),)
47 return u'%s [%s]' % (self.content_type.get_object_for_this_type(pk=self.object_id), self.tag)
48 except ObjectDoesNotExist:
49 return u'<deleted> [%s]' % self.tag
52 class Tag(models.Model):
53 """A tag attachable to books and fragments (and possibly anything).
55 Used to represent searchable metadata (authors, epochs, genres, kinds),
56 fragment themes (motifs) and some book hierarchy related kludges."""
57 name = models.CharField(_('name'), max_length=120, db_index=True)
58 slug = models.SlugField(_('slug'), max_length=120, db_index=True)
59 sort_key = models.CharField(_('sort key'), max_length=120, db_index=True)
60 category = models.CharField(
61 _('category'), max_length=50, blank=False, null=False, db_index=True, choices=TAG_CATEGORIES)
62 description = models.TextField(_('description'), blank=True)
64 for_books = models.BooleanField(default=False)
65 for_pictures = models.BooleanField(default=False)
67 user = models.ForeignKey(User, models.CASCADE, blank=True, null=True)
68 gazeta_link = models.CharField(blank=True, max_length=240)
69 culturepl_link = models.CharField(blank=True, max_length=240)
70 wiki_link = models.CharField(blank=True, max_length=240)
72 created_at = models.DateTimeField(_('creation date'), auto_now_add=True, db_index=True)
73 changed_at = models.DateTimeField(_('creation date'), auto_now=True, db_index=True)
75 after_change = Signal(providing_args=['instance', 'languages'])
77 intermediary_table_model = TagRelation
78 objects = TagManager()
80 class UrlDeprecationWarning(DeprecationWarning):
81 def __init__(self, tags=None):
82 super(Tag.UrlDeprecationWarning, self).__init__()
94 categories_dict = dict((item[::-1] for item in categories_rev.items()))
97 ordering = ('sort_key',)
98 verbose_name = _('tag')
99 verbose_name_plural = _('tags')
100 unique_together = (("slug", "category"),)
101 app_label = 'catalogue'
103 def save(self, *args, **kwargs):
104 flush_cache = flush_all_includes = False
105 if self.pk and self.category != 'set':
106 # Flush the whole views cache.
107 # Seem a little harsh, but changed tag names, descriptions
108 # and links come up at any number of places.
111 # Find in which languages we need to flush related includes.
112 old_self = type(self).objects.get(pk=self.pk)
113 # Category shouldn't normally be changed, but just in case.
114 if self.category != old_self.category:
115 flush_all_includes = True
116 languages_changed = self.languages_changed(old_self)
118 ret = super(Tag, self).save(*args, **kwargs)
121 caches[settings.CACHE_MIDDLEWARE_ALIAS].clear()
122 if flush_all_includes:
125 self.flush_includes()
126 self.after_change.send(sender=type(self), instance=self, languages=languages_changed)
130 def languages_changed(self, old):
131 all_langs = [lc for (lc, _ln) in settings.LANGUAGES]
132 if (old.category, old.slug) != (self.category, self.slug):
135 for lang in all_langs:
136 name_field = 'name_%s' % lang
137 if getattr(old, name_field) != getattr(self, name_field):
141 def flush_includes(self, languages=True):
144 if languages is True:
145 languages = [lc for (lc, _ln) in settings.LANGUAGES]
147 template % (self.pk, lang)
149 '/api/include/tag/%d.%s.json',
150 '/api/include/tag/%d.%s.xml',
152 for lang in languages
155 '/katalog/%s.json' % lang for lang in languages])
161 return "Tag(slug=%r)" % self.slug
163 def get_initial(self):
164 if self.category == 'author':
165 return self.sort_key[0]
172 def category_plural(self):
173 return self.category + 's'
175 def get_absolute_url(self):
176 return reverse('tagged_object_list', args=[self.url_chunk])
178 def get_absolute_gallery_url(self):
179 return reverse('tagged_object_list_gallery', args=[self.url_chunk])
181 def has_description(self):
182 return len(self.description) > 0
183 has_description.short_description = _('description')
184 has_description.boolean = True
187 def get_tag_list(tag_str):
194 tags_splitted = tag_str.split('/')
195 for name in tags_splitted:
197 tags.append(Tag.objects.get(slug=name, category=category))
199 elif name in Tag.categories_rev:
200 category = Tag.categories_rev[name]
203 tags.append(Tag.objects.get(slug=name))
205 except Tag.MultipleObjectsReturned:
206 ambiguous_slugs.append(name)
209 # something strange left off
210 raise Tag.DoesNotExist()
212 # some tags should be qualified
213 e = Tag.MultipleObjectsReturned()
215 e.ambiguous_slugs = ambiguous_slugs
218 raise Tag.UrlDeprecationWarning(tags=tags)
223 return '/'.join((Tag.categories_dict[self.category], self.slug))
226 def tags_from_info(info):
227 from slugify import slugify
228 from sortify import sortify
230 categories = (('kinds', 'kind'), ('genres', 'genre'), ('authors', 'author'), ('epochs', 'epoch'))
231 for field_name, category in categories:
233 tag_names = getattr(info, field_name)
234 except (AttributeError, KeyError): # TODO: shouldn't be KeyError here at all.
236 tag_names = [getattr(info, category)]
238 # For instance, Pictures do not have 'genre' field.
240 for tag_name in tag_names:
241 lang = getattr(tag_name, 'lang', settings.LANGUAGE_CODE)
242 tag_sort_key = tag_name
243 if category == 'author':
244 tag_sort_key = ' '.join((tag_name.last_name,) + tag_name.first_names)
245 tag_name = tag_name.readable()
246 if lang == settings.LANGUAGE_CODE:
247 # Allow creating new tag, if it's in default language.
248 tag, created = Tag.objects.get_or_create(slug=slugify(tag_name), category=category)
250 tag_name = str(tag_name)
252 setattr(tag, "name_%s" % lang, tag_name)
253 tag.sort_key = sortify(tag_sort_key.lower())
256 meta_tags.append(tag)
258 # Ignore unknown tags in non-default languages.
260 tag = Tag.objects.get(category=category, **{"name_%s" % lang: tag_name})
261 except Tag.DoesNotExist:
264 meta_tags.append(tag)
268 TagRelation.tag_model = Tag
271 def prefetch_relations(objects, category, only_name=True):
272 queryset = TagRelation.objects.filter(tag__category=category).select_related('tag')
274 queryset = queryset.only('tag__name_pl', 'object_id')
275 return objects.prefetch_related(
276 Prefetch('tag_relations', queryset=queryset, to_attr='%s_relations' % category))
279 def prefetched_relations(obj, category):
280 if hasattr(obj, '%s_relations' % category):
281 return getattr(obj, '%s_relations' % category)