1 # This file is part of Wolne Lektury, licensed under GNU Affero GPLv3 or later.
2 # Copyright © Fundacja Wolne Lektury. 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
20 ('author', _('autor')),
21 ('epoch', _('epoka')),
22 ('kind', _('rodzaj')),
23 ('genre', _('gatunek')),
24 ('theme', _('motyw')),
29 class TagRelation(models.Model):
30 tag = models.ForeignKey('Tag', models.CASCADE, verbose_name='tag', related_name='items')
31 content_type = models.ForeignKey(ContentType, models.CASCADE, verbose_name='typ obiektu')
32 object_id = models.PositiveIntegerField('id obiektu', db_index=True)
33 content_object = GenericForeignKey('content_type', 'object_id')
35 objects = TaggedItemManager()
38 db_table = 'catalogue_tag_relation'
39 unique_together = (('tag', 'content_type', 'object_id'),)
43 return '%s [%s]' % (self.content_type.get_object_for_this_type(pk=self.object_id), self.tag)
44 except ObjectDoesNotExist:
45 return '<deleted> [%s]' % self.tag
48 class Tag(models.Model):
49 """A tag attachable to books and fragments (and possibly anything).
51 Used to represent searchable metadata (authors, epochs, genres, kinds),
52 fragment themes (motifs) and some book hierarchy related kludges."""
53 name = models.CharField('nazwa', max_length=120, db_index=True)
54 slug = models.SlugField('slug', max_length=120, db_index=True)
55 sort_key = models.CharField('klucz sortowania', max_length=120, db_index=True)
56 category = models.CharField(
57 'kategoria', max_length=50, blank=False, null=False, db_index=True, choices=TAG_CATEGORIES)
58 description = models.TextField('opis', blank=True)
60 user = models.ForeignKey(User, models.CASCADE, blank=True, null=True)
61 gazeta_link = models.CharField(blank=True, max_length=240)
62 culturepl_link = models.CharField(blank=True, max_length=240)
63 wiki_link = models.CharField(blank=True, max_length=240)
64 photo = models.FileField(blank=True, null=True, upload_to='catalogue/tag/')
65 photo_attribution = models.CharField(max_length=255, blank=True)
67 created_at = models.DateTimeField('data utworzenia', auto_now_add=True, db_index=True)
68 changed_at = models.DateTimeField('data modyfikacji', auto_now=True, db_index=True)
70 plural = models.CharField(
71 'liczba mnoga', max_length=255, blank=True,
72 help_text='dotyczy gatunków'
74 genre_epoch_specific = models.BooleanField(
76 help_text='Po wskazaniu tego gatunku, dodanie epoki byłoby nadmiarowe, np. „dramat romantyczny”'
78 adjective_feminine_singular = models.CharField(
79 'przymiotnik pojedynczy żeński', max_length=255, blank=True,
80 help_text='twórczość … Adama Mickiewicza; dotyczy epok'
82 adjective_nonmasculine_plural = models.CharField(
83 'przymiotnik mnogi niemęskoosobowy', max_length=255, blank=True,
84 help_text='utwory … Adama Mickiewicza; dotyczy epok'
86 genitive = models.CharField(
87 'dopełniacz', max_length=255, blank=True,
88 help_text='utwory … (czyje?); dotyczy autorów'
90 collective_noun = models.CharField(
91 'określenie zbiorowe', max_length=255, blank=True,
92 help_text='np. „Liryka” albo „Twórczość dramatyczna”; dotyczy rodzajów'
95 after_change = Signal()
97 intermediary_table_model = TagRelation
98 objects = TagManager()
100 class UrlDeprecationWarning(DeprecationWarning):
101 def __init__(self, tags=None):
102 super(Tag.UrlDeprecationWarning, self).__init__()
113 categories_dict = dict((item[::-1] for item in categories_rev.items()))
116 ordering = ('sort_key',)
118 verbose_name_plural = 'tagi'
119 unique_together = (("slug", "category"),)
120 app_label = 'catalogue'
122 def save(self, *args, quick=False, **kwargs):
123 existing = self.pk and self.category != 'set'
124 ret = super(Tag, self).save(*args, **kwargs)
125 if existing and not quick:
126 self.after_change.send(sender=type(self), instance=self)
133 return "Tag(slug=%r)" % self.slug
135 def get_initial(self):
136 if self.category == 'author':
137 return self.sort_key[0]
144 def category_plural(self):
145 return self.category + 's'
147 def get_absolute_url(self):
148 return reverse('tagged_object_list', args=[self.url_chunk])
150 def get_absolute_catalogue_url(self):
151 # TODO: remove magic.
152 if self.category == 'set':
153 return reverse('social_my_shelf')
155 return reverse(f'{self.category}_catalogue')
157 def has_description(self):
158 return len(self.description) > 0
159 has_description.short_description = 'opis'
160 has_description.boolean = True
163 def get_tag_list(tag_str):
170 tags_splitted = tag_str.split('/')
171 for name in tags_splitted:
173 tags.append(Tag.objects.get(slug=name, category=category))
175 elif name in Tag.categories_rev:
176 category = Tag.categories_rev[name]
179 tags.append(Tag.objects.get(slug=name))
181 except Tag.MultipleObjectsReturned:
182 ambiguous_slugs.append(name)
185 # something strange left off
186 raise Tag.DoesNotExist()
188 # some tags should be qualified
189 e = Tag.MultipleObjectsReturned()
191 e.ambiguous_slugs = ambiguous_slugs
194 raise Tag.UrlDeprecationWarning(tags=tags)
199 return '/'.join((Tag.categories_dict[self.category], self.slug))
202 def tags_from_info(info):
203 from slugify import slugify
204 from sortify import sortify
207 # BookInfo field names, Tag category, relationship
208 ('kinds', 'kind', None),
209 ('genres', 'genre', None),
210 ('epochs', 'epoch', None),
211 ('authors', 'author', None),
212 ('translators', 'author', 'translator'),
214 for field_name, category, relationship in categories:
216 tag_names = getattr(info, field_name)
217 except (AttributeError, KeyError): # TODO: shouldn't be KeyError here at all.
218 # For instance, Pictures do not have 'genre' field.
220 for tag_name in tag_names:
221 lang = getattr(tag_name, 'lang', None) or settings.LANGUAGE_CODE
222 tag_sort_key = tag_name
223 if category == 'author':
224 tag_sort_key = ' '.join((tag_name.last_name,) + tag_name.first_names)
225 tag_name = tag_name.readable()
228 tag = Tag.objects.get(category=category, **{"name_%s" % lang: tag_name})
229 except Tag.DoesNotExist:
230 if lang == settings.LANGUAGE_CODE:
231 # Allow creating new tag, if it's in default language.
232 tag, created = Tag.objects.get_or_create(slug=slugify(tag_name), category=category)
234 tag_name = str(tag_name)
236 setattr(tag, "name_%s" % lang, tag_name)
237 tag.sort_key = sortify(tag_sort_key.lower())
240 meta_tags.append((tag, relationship))
242 meta_tags.append((tag, relationship))
246 TagRelation.tag_model = Tag
249 def prefetch_relations(objects, category, only_name=True):
250 queryset = TagRelation.objects.filter(tag__category=category).select_related('tag')
252 queryset = queryset.only('tag__name_pl', 'object_id')
253 return objects.prefetch_related(
254 Prefetch('tag_relations', queryset=queryset, to_attr='%s_relations' % category))
257 def prefetched_relations(obj, category):
258 if hasattr(obj, '%s_relations' % category):
259 return getattr(obj, '%s_relations' % category)