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