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 gettext_lazy as _
16 from newtagging.models import TagManager, TaggedItemManager
19 # Those are hard-coded here so that makemessages sees them.
21 ('author', _('author')),
22 ('epoch', _('epoch')),
24 ('genre', _('genre')),
25 ('theme', _('theme')),
27 ('thing', _('thing')), # things shown on pictures
31 class TagRelation(models.Model):
33 tag = models.ForeignKey('Tag', models.CASCADE, verbose_name=_('tag'), related_name='items')
34 content_type = models.ForeignKey(ContentType, models.CASCADE, verbose_name=_('content type'))
35 object_id = models.PositiveIntegerField(_('object id'), db_index=True)
36 content_object = GenericForeignKey('content_type', 'object_id')
38 objects = TaggedItemManager()
41 db_table = 'catalogue_tag_relation'
42 unique_together = (('tag', 'content_type', 'object_id'),)
46 return '%s [%s]' % (self.content_type.get_object_for_this_type(pk=self.object_id), self.tag)
47 except ObjectDoesNotExist:
48 return '<deleted> [%s]' % self.tag
51 class Tag(models.Model):
52 """A tag attachable to books and fragments (and possibly anything).
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)
63 for_books = models.BooleanField(default=False)
64 for_pictures = models.BooleanField(default=False)
66 user = models.ForeignKey(User, models.CASCADE, 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 photo = models.FileField(blank=True, null=True, upload_to='catalogue/tag/')
71 photo_attribution = models.CharField(max_length=255, blank=True)
73 created_at = models.DateTimeField(_('creation date'), auto_now_add=True, db_index=True)
74 changed_at = models.DateTimeField(_('creation date'), auto_now=True, db_index=True)
76 plural = models.CharField(
77 'liczba mnoga', max_length=255, blank=True,
78 help_text='dotyczy gatunków'
80 genre_epoch_specific = models.BooleanField(
82 help_text='Po wskazaniu tego gatunku, dodanie epoki byłoby nadmiarowe, np. „dramat romantyczny”'
84 adjective_feminine_singular = models.CharField(
85 'przymiotnik pojedynczy żeński', max_length=255, blank=True,
86 help_text='twórczość … Adama Mickiewicza; dotyczy epok'
88 adjective_nonmasculine_plural = models.CharField(
89 'przymiotnik mnogi niemęskoosobowy', max_length=255, blank=True,
90 help_text='utwory … Adama Mickiewicza; dotyczy epok'
92 genitive = models.CharField(
93 'dopełniacz', max_length=255, blank=True,
94 help_text='utwory … (czyje?); dotyczy autorów'
96 collective_noun = models.CharField(
97 'określenie zbiorowe', max_length=255, blank=True,
98 help_text='np. „Liryka” albo „Twórczość dramatyczna”; dotyczy rodzajów'
101 after_change = Signal()
103 intermediary_table_model = TagRelation
104 objects = TagManager()
106 class UrlDeprecationWarning(DeprecationWarning):
107 def __init__(self, tags=None):
108 super(Tag.UrlDeprecationWarning, self).__init__()
120 categories_dict = dict((item[::-1] for item in categories_rev.items()))
123 ordering = ('sort_key',)
124 verbose_name = _('tag')
125 verbose_name_plural = _('tags')
126 unique_together = (("slug", "category"),)
127 app_label = 'catalogue'
129 def save(self, *args, quick=False, **kwargs):
130 existing = self.pk and self.category != 'set'
131 ret = super(Tag, self).save(*args, **kwargs)
132 if existing and not quick:
133 self.after_change.send(sender=type(self), instance=self)
140 return "Tag(slug=%r)" % self.slug
142 def get_initial(self):
143 if self.category == 'author':
144 return self.sort_key[0]
151 def category_plural(self):
152 return self.category + 's'
154 def get_absolute_url(self):
155 return reverse('tagged_object_list', args=[self.url_chunk])
157 def get_absolute_gallery_url(self):
158 return reverse('tagged_object_list_gallery', args=[self.url_chunk])
160 def get_absolute_catalogue_url(self):
161 # TODO: remove magic.
162 if self.category == 'set':
163 return reverse('social_my_shelf')
164 elif self.category == 'thing':
167 return reverse(f'{self.category}_catalogue')
169 def has_description(self):
170 return len(self.description) > 0
171 has_description.short_description = _('description')
172 has_description.boolean = True
175 def get_tag_list(tag_str):
182 tags_splitted = tag_str.split('/')
183 for name in tags_splitted:
185 tags.append(Tag.objects.get(slug=name, category=category))
187 elif name in Tag.categories_rev:
188 category = Tag.categories_rev[name]
191 tags.append(Tag.objects.get(slug=name))
193 except Tag.MultipleObjectsReturned:
194 ambiguous_slugs.append(name)
197 # something strange left off
198 raise Tag.DoesNotExist()
200 # some tags should be qualified
201 e = Tag.MultipleObjectsReturned()
203 e.ambiguous_slugs = ambiguous_slugs
206 raise Tag.UrlDeprecationWarning(tags=tags)
211 return '/'.join((Tag.categories_dict[self.category], self.slug))
214 def tags_from_info(info):
215 from slugify import slugify
216 from sortify import sortify
218 categories = (('kinds', 'kind'), ('genres', 'genre'), ('authors', 'author'), ('epochs', 'epoch'))
219 for field_name, category in categories:
221 tag_names = getattr(info, field_name)
222 except (AttributeError, KeyError): # TODO: shouldn't be KeyError here at all.
224 tag_names = [getattr(info, category)]
226 # For instance, Pictures do not have 'genre' field.
228 for tag_name in tag_names:
229 lang = getattr(tag_name, 'lang', None) or settings.LANGUAGE_CODE
230 tag_sort_key = tag_name
231 if category == 'author':
232 tag_sort_key = ' '.join((tag_name.last_name,) + tag_name.first_names)
233 tag_name = tag_name.readable()
236 tag = Tag.objects.get(category=category, **{"name_%s" % lang: tag_name})
237 except Tag.DoesNotExist:
238 if lang == settings.LANGUAGE_CODE:
239 # Allow creating new tag, if it's in default language.
240 tag, created = Tag.objects.get_or_create(slug=slugify(tag_name), category=category)
242 tag_name = str(tag_name)
244 setattr(tag, "name_%s" % lang, tag_name)
245 tag.sort_key = sortify(tag_sort_key.lower())
248 meta_tags.append(tag)
250 meta_tags.append(tag)
254 TagRelation.tag_model = Tag
257 def prefetch_relations(objects, category, only_name=True):
258 queryset = TagRelation.objects.filter(tag__category=category).select_related('tag')
260 queryset = queryset.only('tag__name_pl', 'object_id')
261 return objects.prefetch_related(
262 Prefetch('tag_relations', queryset=queryset, to_attr='%s_relations' % category))
265 def prefetched_relations(obj, category):
266 if hasattr(obj, '%s_relations' % category):
267 return getattr(obj, '%s_relations' % category)