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 self._short_html = ''
165 book = super(Book, self).save(force_insert, force_update)
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)
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}
183 def has_description(self):
184 return len(self.description) > 0
185 has_description.short_description = _('description')
186 has_description.boolean = True
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
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
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
203 class AlreadyExists(Exception):
207 def from_xml_file(xml_file, overwrite=False):
208 from tempfile import NamedTemporaryFile
209 from slughifi import slughifi
210 from markupstring import MarkupString
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)
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'))
225 book.title = book_info.title
226 book.set_extra_info_value(book_info.to_dict())
227 book._short_html = ''
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))
240 tag.sort_key = slughifi(tag_sort_key)
241 tag.category = category
243 book_tags.append(tag)
245 book_tag, created = Tag.objects.get_or_create(slug=('l-' + book.slug)[:120])
247 book_tag.name = book.title[:50]
248 book_tag.sort_key = ('l-' + book.slug)[:120]
249 book_tag.category = 'book'
251 book_tags.append(book_tag)
253 book.tags = book_tags
255 if hasattr(book_info, 'parts'):
256 for n, part_url in enumerate(book_info.parts):
257 base, slug = part_url.rsplit('/', 1)
259 child_book = Book.objects.get(slug=slug)
260 child_book.parent = book
261 child_book.parent_number = n
263 except Book.DoesNotExist, e:
264 raise Book.DoesNotExist(_('Book with slug = "%s" does not exist.') % slug)
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())
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)
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)
283 closed_fragments, open_fragments = html.extract_fragments(book.html_file.path)
285 for fragment in closed_fragments.values():
286 text = fragment.to_string()
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})
294 theme_names = [s.strip() for s in fragment.themes.split(',')]
295 except AttributeError:
298 for theme_name in theme_names:
299 tag, created = Tag.objects.get_or_create(slug=slughifi(theme_name))
301 tag.name = theme_name
302 tag.sort_key = slughifi(theme_name)
303 tag.category = 'theme'
307 new_fragment.tags = set(list(book.tags) + themes + [book_tag])
308 book_themes += themes
310 book_themes = set(book_themes)
311 book.tags = list(book.tags) + list(book_themes) + book_shelves
317 def get_absolute_url(self):
318 return ('catalogue.views.book_detail', [self.slug])
321 ordering = ('title',)
322 verbose_name = _('book')
323 verbose_name_plural = _('books')
325 def __unicode__(self):
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')
336 objects = models.Manager()
337 tagged = managers.ModelTaggedItemManager(Tag)
338 tags = managers.TagDescriptor(Tag)
340 def short_html(self):
341 if len(self._short_html):
342 return mark_safe(self._short_html)
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']
347 self._short_html = unicode(render_to_string('catalogue/fragment_short.html',
348 {'fragment': self, 'book': self.book, 'book_authors': book_authors}))
350 return mark_safe(self._short_html)
352 def get_absolute_url(self):
353 return '%s#m%s' % (reverse('book_text', kwargs={'slug': self.book.slug}), self.anchor)
356 ordering = ('book', 'anchor',)
357 verbose_name = _('fragment')
358 verbose_name_plural = _('fragments')
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)
370 return self.pd is not None and self.pd <= datetime.now().year
377 def get_absolute_url(self):
378 return ('catalogue.views.book_detail', [self.slug])
380 def __unicode__(self):
384 ordering = ('title',)
385 verbose_name = _('book stub')
386 verbose_name_plural = _('book stubs')