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.
 
   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
 
  15 from newtagging.models import TagBase
 
  16 from newtagging import managers
 
  17 from catalogue.fields import JSONField
 
  19 from librarian import html, dcparser
 
  20 from mutagen import id3
 
  24     ('author', _('author')),
 
  25     ('epoch', _('epoch')),
 
  27     ('genre', _('genre')),
 
  28     ('theme', _('theme')),
 
  34 class TagSubcategoryManager(models.Manager):
 
  35     def __init__(self, subcategory):
 
  36         super(TagSubcategoryManager, self).__init__()
 
  37         self.subcategory = subcategory
 
  39     def get_query_set(self):
 
  40         return super(TagSubcategoryManager, self).get_query_set().filter(category=self.subcategory)
 
  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'))
 
  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)
 
  58     def has_description(self):
 
  59         return len(self.description) > 0
 
  60     has_description.short_description = _('description')
 
  61     has_description.boolean = True
 
  64         return self.death is None
 
  67         """ tests whether an author is in public domain """
 
  68         return self.death is not None and self.goes_to_pd() <= datetime.now().year
 
  71         """ calculates the year of public domain entry for an author """
 
  72         return self.death + 71 if self.death is not None else None
 
  75     def get_absolute_url(self):
 
  76         return ('catalogue.views.tagged_object_list', [self.slug])
 
  79         ordering = ('sort_key',)
 
  80         verbose_name = _('tag')
 
  81         verbose_name_plural = _('tags')
 
  83     def __unicode__(self):
 
  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]
 
  92             return TagBase.get_tag_list(tags)
 
  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
 
 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)
 
 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)
 
 122     parent = models.ForeignKey('self', blank=True, null=True, related_name='children')
 
 124     objects = models.Manager()
 
 125     tagged = managers.ModelTaggedItemManager(Tag)
 
 126     tags = managers.TagDescriptor(Tag)
 
 132     def short_html(self):
 
 133         if len(self._short_html):
 
 134             return mark_safe(self._short_html)
 
 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]
 
 141                 formats.append(u'<a href="%s">%s</a>' % (reverse('book_text', kwargs={'slug': self.slug}), _('Read online')))
 
 143                 formats.append(u'<a href="%s">PDF</a>' % self.pdf_file.url)
 
 145                 formats.append(u'<a href="%s">ODT</a>' % self.odt_file.url)
 
 147                 formats.append(u'<a href="%s">TXT</a>' % self.txt_file.url)
 
 149                 formats.append(u'<a href="%s">MP3</a>' % self.mp3_file.url)
 
 151                 formats.append(u'<a href="%s">OGG</a>' % self.ogg_file.url)
 
 153             formats = [mark_safe(format) for format in formats]
 
 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)
 
 160     def save(self, force_insert=False, force_update=False, reset_short_html=True):
 
 162             # Reset _short_html during save
 
 163             for key in filter(lambda x: x.startswith('_short_html'), self.__dict__):
 
 166         book = super(Book, self).save(force_insert, force_update)
 
 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)
 
 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}
 
 184     def has_description(self):
 
 185         return len(self.description) > 0
 
 186     has_description.short_description = _('description')
 
 187     has_description.boolean = True
 
 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
 
 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
 
 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
 
 204     class AlreadyExists(Exception):
 
 208     def from_xml_file(xml_file, overwrite=False):
 
 209         from tempfile import NamedTemporaryFile
 
 210         from slughifi import slughifi
 
 211         from markupstring import MarkupString
 
 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)
 
 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'))
 
 226         book.title = book_info.title
 
 227         book.set_extra_info_value(book_info.to_dict())
 
 228         book._short_html = ''
 
 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))
 
 241                 tag.sort_key = slughifi(tag_sort_key)
 
 242                 tag.category = category
 
 244             book_tags.append(tag)
 
 246         book_tag, created = Tag.objects.get_or_create(slug=('l-' + book.slug)[:120])
 
 248             book_tag.name = book.title[:50]
 
 249             book_tag.sort_key = ('l-' + book.slug)[:120]
 
 250             book_tag.category = 'book'
 
 252         book_tags.append(book_tag)
 
 254         book.tags = book_tags
 
 256         if hasattr(book_info, 'parts'):
 
 257             for n, part_url in enumerate(book_info.parts):
 
 258                 base, slug = part_url.rsplit('/', 1)
 
 260                     child_book = Book.objects.get(slug=slug)
 
 261                     child_book.parent = book
 
 262                     child_book.parent_number = n
 
 264                 except Book.DoesNotExist, e:
 
 265                     raise Book.DoesNotExist(_('Book with slug = "%s" does not exist.') % slug)
 
 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())
 
 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)
 
 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)
 
 284             closed_fragments, open_fragments = html.extract_fragments(book.html_file.path)
 
 286             for fragment in closed_fragments.values():
 
 287                 text = fragment.to_string()
 
 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})
 
 295                     theme_names = [s.strip() for s in fragment.themes.split(',')]
 
 296                 except AttributeError:
 
 299                 for theme_name in theme_names:
 
 300                     tag, created = Tag.objects.get_or_create(slug=slughifi(theme_name))
 
 302                         tag.name = theme_name
 
 303                         tag.sort_key = slughifi(theme_name)
 
 304                         tag.category = 'theme'
 
 308                 new_fragment.tags = set(list(book.tags) + themes + [book_tag])
 
 309                 book_themes += themes
 
 311             book_themes = set(book_themes)
 
 312             book.tags = list(book.tags) + list(book_themes) + book_shelves
 
 318     def get_absolute_url(self):
 
 319         return ('catalogue.views.book_detail', [self.slug])
 
 322         ordering = ('title',)
 
 323         verbose_name = _('book')
 
 324         verbose_name_plural = _('books')
 
 326     def __unicode__(self):
 
 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')
 
 337     objects = models.Manager()
 
 338     tagged = managers.ModelTaggedItemManager(Tag)
 
 339     tags = managers.TagDescriptor(Tag)
 
 341     def short_html(self):
 
 342         if len(self._short_html):
 
 343             return mark_safe(self._short_html)
 
 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']
 
 348             self._short_html = unicode(render_to_string('catalogue/fragment_short.html',
 
 349                 {'fragment': self, 'book': self.book, 'book_authors': book_authors}))
 
 351             return mark_safe(self._short_html)
 
 353     def get_absolute_url(self):
 
 354         return '%s#m%s' % (reverse('book_text', kwargs={'slug': self.book.slug}), self.anchor)
 
 357         ordering = ('book', 'anchor',)
 
 358         verbose_name = _('fragment')
 
 359         verbose_name_plural = _('fragments')
 
 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)
 
 371         return self.pd is not None and self.pd <= datetime.now().year
 
 378     def get_absolute_url(self):
 
 379         return ('catalogue.views.book_detail', [self.slug])
 
 381     def __unicode__(self):
 
 385         ordering = ('title',)
 
 386         verbose_name = _('book stub')
 
 387         verbose_name_plural = _('book stubs')