some translation stuff
[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             for key in filter(lambda x: x.startswith('_short_html'), self.__dict__):
164                 self.key = ''
165         
166         book = super(Book, self).save(force_insert, force_update)
167         
168         if self.mp3_file:
169             print self.mp3_file, self.mp3_file.path
170             extra_info = self.get_extra_info_value()
171             extra_info.update(self.get_mp3_info())
172             self.set_extra_info_value(extra_info)
173             book = super(Book, self).save(force_insert, force_update)
174         
175         return book
176     
177     def get_mp3_info(self):
178         """Retrieves artist and director names from audio ID3 tags."""
179         audio = id3.ID3(self.mp3_file.path)
180         artist_name = ', '.join(', '.join(tag.text) for tag in audio.getall('TPE1'))
181         director_name = ', '.join(', '.join(tag.text) for tag in audio.getall('TPE3'))
182         return {'artist_name': artist_name, 'director_name': director_name}
183         
184     def has_description(self):
185         return len(self.description) > 0
186     has_description.short_description = _('description')
187     has_description.boolean = True
188     
189     def has_pdf_file(self):
190         return bool(self.pdf_file)
191     has_pdf_file.short_description = 'PDF'
192     has_pdf_file.boolean = True
193     
194     def has_odt_file(self):
195         return bool(self.odt_file)
196     has_odt_file.short_description = 'ODT'
197     has_odt_file.boolean = True
198     
199     def has_html_file(self):
200         return bool(self.html_file)
201     has_html_file.short_description = 'HTML'
202     has_html_file.boolean = True
203
204     class AlreadyExists(Exception):
205         pass
206     
207     @staticmethod
208     def from_xml_file(xml_file, overwrite=False):
209         from tempfile import NamedTemporaryFile
210         from slughifi import slughifi
211         from markupstring import MarkupString
212         
213         # Read book metadata
214         book_info = dcparser.parse(xml_file)
215         book_base, book_slug = book_info.url.rsplit('/', 1)
216         book, created = Book.objects.get_or_create(slug=book_slug)
217         
218         if created:
219             book_shelves = []
220         else:
221             if not overwrite:
222                 raise Book.AlreadyExists(_('Book %s already exists') % book_slug)
223             # Save shelves for this book
224             book_shelves = list(book.tags.filter(category='set'))
225         
226         book.title = book_info.title
227         book.set_extra_info_value(book_info.to_dict())
228         book._short_html = ''
229         book.save()
230         
231         book_tags = []
232         for category in ('kind', 'genre', 'author', 'epoch'):    
233             tag_name = getattr(book_info, category)
234             tag_sort_key = tag_name
235             if category == 'author':
236                 tag_sort_key = tag_name.last_name
237                 tag_name = ' '.join(tag_name.first_names) + ' ' + tag_name.last_name
238             tag, created = Tag.objects.get_or_create(slug=slughifi(tag_name))
239             if created:
240                 tag.name = tag_name
241                 tag.sort_key = slughifi(tag_sort_key)
242                 tag.category = category
243                 tag.save()
244             book_tags.append(tag)
245             
246         book_tag, created = Tag.objects.get_or_create(slug=('l-' + book.slug)[:120])
247         if created:
248             book_tag.name = book.title[:50]
249             book_tag.sort_key = ('l-' + book.slug)[:120]
250             book_tag.category = 'book'
251             book_tag.save()
252         book_tags.append(book_tag)
253         
254         book.tags = book_tags
255         
256         if hasattr(book_info, 'parts'):
257             for n, part_url in enumerate(book_info.parts):
258                 base, slug = part_url.rsplit('/', 1)
259                 try:
260                     child_book = Book.objects.get(slug=slug)
261                     child_book.parent = book
262                     child_book.parent_number = n
263                     child_book.save()
264                 except Book.DoesNotExist, e:
265                     raise Book.DoesNotExist(_('Book with slug = "%s" does not exist.') % slug)
266         
267         book_descendants = list(book.children.all())
268         while len(book_descendants) > 0:
269             child_book = book_descendants.pop(0)
270             for fragment in child_book.fragments.all():
271                 fragment.tags = set(list(fragment.tags) + [book_tag])
272             book_descendants += list(child_book.children.all())
273             
274         # Save XML and HTML files
275         if not isinstance(xml_file, File):
276             xml_file = File(file(xml_file))
277         book.xml_file.save('%s.xml' % book.slug, xml_file, save=False)
278         
279         html_file = NamedTemporaryFile()
280         if html.transform(book.xml_file.path, html_file):
281             book.html_file.save('%s.html' % book.slug, File(html_file), save=False)
282             
283             # Extract fragments
284             closed_fragments, open_fragments = html.extract_fragments(book.html_file.path)
285             book_themes = []
286             for fragment in closed_fragments.values():
287                 text = fragment.to_string()
288                 short_text = ''
289                 if (len(MarkupString(text)) > 240):
290                     short_text = unicode(MarkupString(text)[:160])
291                 new_fragment, created = Fragment.objects.get_or_create(anchor=fragment.id, book=book, 
292                     defaults={'text': text, 'short_text': short_text})
293                 
294                 try:
295                     theme_names = [s.strip() for s in fragment.themes.split(',')]
296                 except AttributeError:
297                     continue
298                 themes = []
299                 for theme_name in theme_names:
300                     tag, created = Tag.objects.get_or_create(slug=slughifi(theme_name))
301                     if created:
302                         tag.name = theme_name
303                         tag.sort_key = slughifi(theme_name)
304                         tag.category = 'theme'
305                         tag.save()
306                     themes.append(tag)
307                 new_fragment.save()
308                 new_fragment.tags = set(list(book.tags) + themes + [book_tag])
309                 book_themes += themes
310             
311             book_themes = set(book_themes)
312             book.tags = list(book.tags) + list(book_themes) + book_shelves
313         
314         book.save()
315         return book
316     
317     @permalink
318     def get_absolute_url(self):
319         return ('catalogue.views.book_detail', [self.slug])
320         
321     class Meta:
322         ordering = ('title',)
323         verbose_name = _('book')
324         verbose_name_plural = _('books')
325
326     def __unicode__(self):
327         return self.title
328
329
330 class Fragment(models.Model):
331     text = models.TextField()
332     short_text = models.TextField(editable=False)
333     _short_html = models.TextField(editable=False)
334     anchor = models.CharField(max_length=120)
335     book = models.ForeignKey(Book, related_name='fragments')
336
337     objects = models.Manager()
338     tagged = managers.ModelTaggedItemManager(Tag)
339     tags = managers.TagDescriptor(Tag)
340     
341     def short_html(self):
342         if len(self._short_html):
343             return mark_safe(self._short_html)
344         else:
345             book_authors = [mark_safe(u'<a href="%s">%s</a>' % (tag.get_absolute_url(), tag.name)) 
346                 for tag in self.book.tags if tag.category == 'author']
347             
348             self._short_html = unicode(render_to_string('catalogue/fragment_short.html',
349                 {'fragment': self, 'book': self.book, 'book_authors': book_authors}))
350             self.save()
351             return mark_safe(self._short_html)
352     
353     def get_absolute_url(self):
354         return '%s#m%s' % (reverse('book_text', kwargs={'slug': self.book.slug}), self.anchor)
355     
356     class Meta:
357         ordering = ('book', 'anchor',)
358         verbose_name = _('fragment')
359         verbose_name_plural = _('fragments')
360
361
362 class BookStub(models.Model):
363     title = models.CharField(_('title'), max_length=120)
364     author = models.CharField(_('author'), max_length=120)
365     pd = models.IntegerField(_('goes to public domain'), null=True, blank=True)
366     slug = models.SlugField(_('slug'), max_length=120, unique=True, db_index=True)
367     translator = models.TextField(_('translator'), blank=True)
368     translator_death = models.TextField(_('year of translator\'s death'), blank=True)
369
370     def in_pd(self):
371         return self.pd is not None and self.pd <= datetime.now().year
372
373     @property
374     def name(self):
375         return self.title
376     
377     @permalink
378     def get_absolute_url(self):
379         return ('catalogue.views.book_detail', [self.slug])
380
381     def __unicode__(self):
382         return self.title
383     
384     class Meta:
385         ordering = ('title',)
386         verbose_name = _('book stub')
387         verbose_name_plural = _('book stubs')