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')