Workaround for tagged_object_list.
[wolnelektury.git] / apps / catalogue / models.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.db import models
6 from django.db.models import permalink, Q
7 from django.utils.translation import ugettext_lazy as _
8 from django.contrib.auth.models import User
9 from django.core.files import File
10 from django.template.loader import render_to_string
11 from django.utils.safestring import mark_safe
12 from django.utils.translation import get_language
13 from django.core.urlresolvers import reverse
14 from datetime import datetime
15
16 from newtagging.models import TagBase
17 from newtagging import managers
18 from catalogue.fields import JSONField
19
20 from librarian import html, dcparser
21 from mutagen import id3
22
23
24 TAG_CATEGORIES = (
25     ('author', _('author')),
26     ('epoch', _('epoch')),
27     ('kind', _('kind')),
28     ('genre', _('genre')),
29     ('theme', _('theme')),
30     ('set', _('set')),
31     ('book', _('book')),
32 )
33
34
35 class TagSubcategoryManager(models.Manager):
36     def __init__(self, subcategory):
37         super(TagSubcategoryManager, self).__init__()
38         self.subcategory = subcategory
39         
40     def get_query_set(self):
41         return super(TagSubcategoryManager, self).get_query_set().filter(category=self.subcategory)
42
43
44 class Tag(TagBase):
45     name = models.CharField(_('name'), max_length=50, db_index=True)
46     slug = models.SlugField(_('slug'), max_length=120, unique=True, db_index=True)
47     sort_key = models.SlugField(_('sort key'), max_length=120, db_index=True)
48     category = models.CharField(_('category'), max_length=50, blank=False, null=False, 
49         db_index=True, choices=TAG_CATEGORIES)
50     description = models.TextField(_('description'), blank=True)
51     main_page = models.BooleanField(_('main page'), default=False, db_index=True, help_text=_('Show tag on main page'))
52     
53     user = models.ForeignKey(User, blank=True, null=True)
54     book_count = models.IntegerField(_('book count'), default=0, blank=False, null=False)
55     death = models.IntegerField(_(u'year of death'), blank=True, null=True)
56     gazeta_link = models.CharField(blank=True,  max_length=240)
57     wiki_link = models.CharField(blank=True,  max_length=240)
58     
59     def has_description(self):
60         return len(self.description) > 0
61     has_description.short_description = _('description')
62     has_description.boolean = True
63
64     def alive(self):
65         return self.death is None
66     
67     def in_pd(self):
68         """ tests whether an author is in public domain """
69         return self.death is not None and self.goes_to_pd() <= datetime.now().year
70     
71     def goes_to_pd(self):
72         """ calculates the year of public domain entry for an author """
73         return self.death + 71 if self.death is not None else None
74
75     @permalink
76     def get_absolute_url(self):
77         return ('catalogue.views.tagged_object_list', [self.slug])
78     
79     class Meta:
80         ordering = ('sort_key',)
81         verbose_name = _('tag')
82         verbose_name_plural = _('tags')
83     
84     def __unicode__(self):
85         return self.name
86
87     @staticmethod
88     def get_tag_list(tags):
89         if isinstance(tags, basestring):
90             tag_slugs = tags.split('/')
91             return [Tag.objects.get(slug=slug) for slug in tag_slugs]
92         else:
93             return TagBase.get_tag_list(tags)
94
95
96 def book_upload_path(ext):
97     def get_dynamic_path(book, filename):
98         return 'lektura/%s.%s' % (book.slug, ext)
99     return get_dynamic_path
100
101
102 class Book(models.Model):
103     title = models.CharField(_('title'), max_length=120)
104     slug = models.SlugField(_('slug'), max_length=120, unique=True, db_index=True)
105     description = models.TextField(_('description'), blank=True)
106     created_at = models.DateTimeField(_('creation date'), auto_now=True)
107     _short_html = models.TextField(_('short HTML'), editable=False)
108     parent_number = models.IntegerField(_('parent number'), default=0)
109     extra_info = JSONField(_('extra information'))
110     gazeta_link = models.CharField(blank=True,  max_length=240)
111     wiki_link = models.CharField(blank=True,  max_length=240)
112
113     
114     # Formats
115     xml_file = models.FileField(_('XML file'), upload_to=book_upload_path('xml'), blank=True)
116     html_file = models.FileField(_('HTML file'), upload_to=book_upload_path('html'), blank=True)
117     pdf_file = models.FileField(_('PDF file'), upload_to=book_upload_path('pdf'), blank=True)
118     odt_file = models.FileField(_('ODT file'), upload_to=book_upload_path('odt'), blank=True)
119     txt_file = models.FileField(_('TXT file'), upload_to=book_upload_path('txt'), blank=True)
120     mp3_file = models.FileField(_('MP3 file'), upload_to=book_upload_path('mp3'), blank=True)
121     ogg_file = models.FileField(_('OGG file'), upload_to=book_upload_path('ogg'), blank=True)
122     
123     parent = models.ForeignKey('self', blank=True, null=True, related_name='children')
124     
125     objects = models.Manager()
126     tagged = managers.ModelTaggedItemManager(Tag)
127     tags = managers.TagDescriptor(Tag)
128     
129     @property
130     def name(self):
131         return self.title
132     
133     def short_html(self):
134         key = '_short_html_%s' % get_language()
135         short_html = getattr(self, key)
136         
137         if short_html and len(short_html):
138             return mark_safe(short_html)
139         else:
140             tags = self.tags.filter(~Q(category__in=('set', 'theme', 'book')))
141             tags = [mark_safe(u'<a href="%s">%s</a>' % (tag.get_absolute_url(), tag.name)) for tag in tags]
142
143             formats = []
144             if self.html_file:
145                 formats.append(u'<a href="%s">%s</a>' % (reverse('book_text', kwargs={'slug': self.slug}), _('Read online')))
146             if self.pdf_file:
147                 formats.append(u'<a href="%s">PDF</a>' % self.pdf_file.url)
148             if self.odt_file:
149                 formats.append(u'<a href="%s">ODT</a>' % self.odt_file.url)
150             if self.txt_file:
151                 formats.append(u'<a href="%s">TXT</a>' % self.txt_file.url)
152             if self.mp3_file:
153                 formats.append(u'<a href="%s">MP3</a>' % self.mp3_file.url)
154             if self.ogg_file:
155                 formats.append(u'<a href="%s">OGG</a>' % self.ogg_file.url)
156             
157             formats = [mark_safe(format) for format in formats]
158             
159             setattr(self, key, unicode(render_to_string('catalogue/book_short.html',
160                 {'book': self, 'tags': tags, 'formats': formats})))
161             self.save(reset_short_html=False)
162             return mark_safe(getattr(self, key))
163     
164     def save(self, force_insert=False, force_update=False, reset_short_html=True):
165         if reset_short_html:
166             # Reset _short_html during save
167             for key in filter(lambda x: x.startswith('_short_html'), self.__dict__):
168                 self.__setattr__(key, '')
169         
170         book = super(Book, self).save(force_insert, force_update)
171         
172         if self.mp3_file:
173             print self.mp3_file, self.mp3_file.path
174             extra_info = self.get_extra_info_value()
175             extra_info.update(self.get_mp3_info())
176             self.set_extra_info_value(extra_info)
177             book = super(Book, self).save(force_insert, force_update)
178         
179         return book
180     
181     def get_mp3_info(self):
182         """Retrieves artist and director names from audio ID3 tags."""
183         audio = id3.ID3(self.mp3_file.path)
184         artist_name = ', '.join(', '.join(tag.text) for tag in audio.getall('TPE1'))
185         director_name = ', '.join(', '.join(tag.text) for tag in audio.getall('TPE3'))
186         return {'artist_name': artist_name, 'director_name': director_name}
187         
188     def has_description(self):
189         return len(self.description) > 0
190     has_description.short_description = _('description')
191     has_description.boolean = True
192     
193     def has_pdf_file(self):
194         return bool(self.pdf_file)
195     has_pdf_file.short_description = 'PDF'
196     has_pdf_file.boolean = True
197     
198     def has_odt_file(self):
199         return bool(self.odt_file)
200     has_odt_file.short_description = 'ODT'
201     has_odt_file.boolean = True
202     
203     def has_html_file(self):
204         return bool(self.html_file)
205     has_html_file.short_description = 'HTML'
206     has_html_file.boolean = True
207
208     class AlreadyExists(Exception):
209         pass
210     
211     @staticmethod
212     def from_xml_file(xml_file, overwrite=False):
213         from tempfile import NamedTemporaryFile
214         from slughifi import slughifi
215         from markupstring import MarkupString
216         
217         # Read book metadata
218         book_info = dcparser.parse(xml_file)
219         book_base, book_slug = book_info.url.rsplit('/', 1)
220         book, created = Book.objects.get_or_create(slug=book_slug)
221         
222         if created:
223             book_shelves = []
224         else:
225             if not overwrite:
226                 raise Book.AlreadyExists(_('Book %s already exists') % book_slug)
227             # Save shelves for this book
228             book_shelves = list(book.tags.filter(category='set'))
229         
230         book.title = book_info.title
231         book.set_extra_info_value(book_info.to_dict())
232         book._short_html = ''
233         book.save()
234         
235         book_tags = []
236         for category in ('kind', 'genre', 'author', 'epoch'):    
237             tag_name = getattr(book_info, category)
238             tag_sort_key = tag_name
239             if category == 'author':
240                 tag_sort_key = tag_name.last_name
241                 tag_name = ' '.join(tag_name.first_names) + ' ' + tag_name.last_name
242             tag, created = Tag.objects.get_or_create(slug=slughifi(tag_name))
243             if created:
244                 tag.name = tag_name
245                 tag.sort_key = slughifi(tag_sort_key)
246                 tag.category = category
247                 tag.save()
248             book_tags.append(tag)
249             
250         book_tag, created = Tag.objects.get_or_create(slug=('l-' + book.slug)[:120])
251         if created:
252             book_tag.name = book.title[:50]
253             book_tag.sort_key = ('l-' + book.slug)[:120]
254             book_tag.category = 'book'
255             book_tag.save()
256         book_tags.append(book_tag)
257         
258         book.tags = book_tags
259         
260         if hasattr(book_info, 'parts'):
261             for n, part_url in enumerate(book_info.parts):
262                 base, slug = part_url.rsplit('/', 1)
263                 try:
264                     child_book = Book.objects.get(slug=slug)
265                     child_book.parent = book
266                     child_book.parent_number = n
267                     child_book.save()
268                 except Book.DoesNotExist, e:
269                     raise Book.DoesNotExist(_('Book with slug = "%s" does not exist.') % slug)
270         
271         book_descendants = list(book.children.all())
272         while len(book_descendants) > 0:
273             child_book = book_descendants.pop(0)
274             for fragment in child_book.fragments.all():
275                 fragment.tags = set(list(fragment.tags) + [book_tag])
276             book_descendants += list(child_book.children.all())
277             
278         # Save XML and HTML files
279         if not isinstance(xml_file, File):
280             xml_file = File(file(xml_file))
281         book.xml_file.save('%s.xml' % book.slug, xml_file, save=False)
282         
283         html_file = NamedTemporaryFile()
284         if html.transform(book.xml_file.path, html_file):
285             book.html_file.save('%s.html' % book.slug, File(html_file), save=False)
286             
287             # Extract fragments
288             closed_fragments, open_fragments = html.extract_fragments(book.html_file.path)
289             book_themes = []
290             for fragment in closed_fragments.values():
291                 text = fragment.to_string()
292                 short_text = ''
293                 if (len(MarkupString(text)) > 240):
294                     short_text = unicode(MarkupString(text)[:160])
295                 new_fragment, created = Fragment.objects.get_or_create(anchor=fragment.id, book=book, 
296                     defaults={'text': text, 'short_text': short_text})
297                 
298                 try:
299                     theme_names = [s.strip() for s in fragment.themes.split(',')]
300                 except AttributeError:
301                     continue
302                 themes = []
303                 for theme_name in theme_names:
304                     tag, created = Tag.objects.get_or_create(slug=slughifi(theme_name))
305                     if created:
306                         tag.name = theme_name
307                         tag.sort_key = slughifi(theme_name)
308                         tag.category = 'theme'
309                         tag.save()
310                     themes.append(tag)
311                 new_fragment.save()
312                 new_fragment.tags = set(list(book.tags) + themes + [book_tag])
313                 book_themes += themes
314             
315             book_themes = set(book_themes)
316             book.tags = list(book.tags) + list(book_themes) + book_shelves
317         
318         book.save()
319         return book
320     
321     @permalink
322     def get_absolute_url(self):
323         return ('catalogue.views.book_detail', [self.slug])
324         
325     class Meta:
326         ordering = ('title',)
327         verbose_name = _('book')
328         verbose_name_plural = _('books')
329
330     def __unicode__(self):
331         return self.title
332
333
334 class Fragment(models.Model):
335     text = models.TextField()
336     short_text = models.TextField(editable=False)
337     _short_html = models.TextField(editable=False)
338     anchor = models.CharField(max_length=120)
339     book = models.ForeignKey(Book, related_name='fragments')
340
341     objects = models.Manager()
342     tagged = managers.ModelTaggedItemManager(Tag)
343     tags = managers.TagDescriptor(Tag)
344     
345     def short_html(self):
346         key = '_short_html_%s' % get_language()
347         short_html = getattr(self, key)         
348         if short_html and len(short_html):
349             return mark_safe(short_html)
350         else:
351             book_authors = [mark_safe(u'<a href="%s">%s</a>' % (tag.get_absolute_url(), tag.name)) 
352                 for tag in self.book.tags if tag.category == 'author']
353             
354             setattr(self, key, unicode(render_to_string('catalogue/fragment_short.html',
355                 {'fragment': self, 'book': self.book, 'book_authors': book_authors})))
356             self.save()
357             return mark_safe(getattr(self, key))
358     
359     def get_absolute_url(self):
360         return '%s#m%s' % (reverse('book_text', kwargs={'slug': self.book.slug}), self.anchor)
361     
362     class Meta:
363         ordering = ('book', 'anchor',)
364         verbose_name = _('fragment')
365         verbose_name_plural = _('fragments')
366
367
368 class BookStub(models.Model):
369     title = models.CharField(_('title'), max_length=120)
370     author = models.CharField(_('author'), max_length=120)
371     pd = models.IntegerField(_('goes to public domain'), null=True, blank=True)
372     slug = models.SlugField(_('slug'), max_length=120, unique=True, db_index=True)
373     translator = models.TextField(_('translator'), blank=True)
374     translator_death = models.TextField(_('year of translator\'s death'), blank=True)
375
376     def in_pd(self):
377         return self.pd is not None and self.pd <= datetime.now().year
378
379     @property
380     def name(self):
381         return self.title
382     
383     @permalink
384     def get_absolute_url(self):
385         return ('catalogue.views.book_detail', [self.slug])
386
387     def __unicode__(self):
388         return self.title
389     
390     class Meta:
391         ordering = ('title',)
392         verbose_name = _('book stub')
393         verbose_name_plural = _('book stubs')