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.utils.translation import get_language
13 from django.core.urlresolvers import reverse
14 from datetime import datetime
16 from newtagging.models import TagBase
17 from newtagging import managers
18 from catalogue.fields import JSONField
20 from librarian import html, dcparser
21 from mutagen import id3
25 ('author', _('author')),
26 ('epoch', _('epoch')),
28 ('genre', _('genre')),
29 ('theme', _('theme')),
35 class TagSubcategoryManager(models.Manager):
36 def __init__(self, subcategory):
37 super(TagSubcategoryManager, self).__init__()
38 self.subcategory = subcategory
40 def get_query_set(self):
41 return super(TagSubcategoryManager, self).get_query_set().filter(category=self.subcategory)
45 name = models.CharField(_('name'), max_length=50, db_index=True)
46 slug = models.SlugField(_('slug'), max_length=120, unique=True, db_index=True)
47 sort_key = models.SlugField(_('sort key'), max_length=120, db_index=True)
48 category = models.CharField(_('category'), max_length=50, blank=False, null=False,
49 db_index=True, choices=TAG_CATEGORIES)
50 description = models.TextField(_('description'), blank=True)
51 main_page = models.BooleanField(_('main page'), default=False, db_index=True, help_text=_('Show tag on main page'))
53 user = models.ForeignKey(User, blank=True, null=True)
54 book_count = models.IntegerField(_('book count'), default=0, blank=False, null=False)
55 death = models.IntegerField(_(u'year of death'), blank=True, null=True)
56 gazeta_link = models.CharField(blank=True, max_length=240)
57 wiki_link = models.CharField(blank=True, max_length=240)
59 def has_description(self):
60 return len(self.description) > 0
61 has_description.short_description = _('description')
62 has_description.boolean = True
65 return self.death is None
68 """ tests whether an author is in public domain """
69 return self.death is not None and self.goes_to_pd() <= datetime.now().year
72 """ calculates the year of public domain entry for an author """
73 return self.death + 71 if self.death is not None else None
76 def get_absolute_url(self):
77 return ('catalogue.views.tagged_object_list', [self.slug])
80 ordering = ('sort_key',)
81 verbose_name = _('tag')
82 verbose_name_plural = _('tags')
84 def __unicode__(self):
88 def get_tag_list(tags):
89 if isinstance(tags, basestring):
90 tag_slugs = tags.split('/')
91 return [Tag.objects.get(slug=slug) for slug in tag_slugs]
93 return TagBase.get_tag_list(tags)
96 def book_upload_path(ext):
97 def get_dynamic_path(book, filename):
98 return 'lektura/%s.%s' % (book.slug, ext)
99 return get_dynamic_path
102 class Book(models.Model):
103 title = models.CharField(_('title'), max_length=120)
104 slug = models.SlugField(_('slug'), max_length=120, unique=True, db_index=True)
105 description = models.TextField(_('description'), blank=True)
106 created_at = models.DateTimeField(_('creation date'), auto_now=True)
107 _short_html = models.TextField(_('short HTML'), editable=False)
108 parent_number = models.IntegerField(_('parent number'), default=0)
109 extra_info = JSONField(_('extra information'))
110 gazeta_link = models.CharField(blank=True, max_length=240)
111 wiki_link = models.CharField(blank=True, max_length=240)
115 xml_file = models.FileField(_('XML file'), upload_to=book_upload_path('xml'), blank=True)
116 html_file = models.FileField(_('HTML file'), upload_to=book_upload_path('html'), blank=True)
117 pdf_file = models.FileField(_('PDF file'), upload_to=book_upload_path('pdf'), blank=True)
118 odt_file = models.FileField(_('ODT file'), upload_to=book_upload_path('odt'), blank=True)
119 txt_file = models.FileField(_('TXT file'), upload_to=book_upload_path('txt'), blank=True)
120 mp3_file = models.FileField(_('MP3 file'), upload_to=book_upload_path('mp3'), blank=True)
121 ogg_file = models.FileField(_('OGG file'), upload_to=book_upload_path('ogg'), blank=True)
123 parent = models.ForeignKey('self', blank=True, null=True, related_name='children')
125 objects = models.Manager()
126 tagged = managers.ModelTaggedItemManager(Tag)
127 tags = managers.TagDescriptor(Tag)
133 def short_html(self):
134 key = '_short_html_%s' % get_language()
135 short_html = getattr(self, key)
137 if short_html and len(short_html):
138 return mark_safe(short_html)
140 tags = self.tags.filter(~Q(category__in=('set', 'theme', 'book')))
141 tags = [mark_safe(u'<a href="%s">%s</a>' % (tag.get_absolute_url(), tag.name)) for tag in tags]
145 formats.append(u'<a href="%s">%s</a>' % (reverse('book_text', kwargs={'slug': self.slug}), _('Read online')))
147 formats.append(u'<a href="%s">PDF</a>' % self.pdf_file.url)
149 formats.append(u'<a href="%s">ODT</a>' % self.odt_file.url)
151 formats.append(u'<a href="%s">TXT</a>' % self.txt_file.url)
153 formats.append(u'<a href="%s">MP3</a>' % self.mp3_file.url)
155 formats.append(u'<a href="%s">OGG</a>' % self.ogg_file.url)
157 formats = [mark_safe(format) for format in formats]
159 setattr(self, key, unicode(render_to_string('catalogue/book_short.html',
160 {'book': self, 'tags': tags, 'formats': formats})))
161 self.save(reset_short_html=False)
162 return mark_safe(getattr(self, key))
164 def save(self, force_insert=False, force_update=False, reset_short_html=True):
166 # Reset _short_html during save
167 for key in filter(lambda x: x.startswith('_short_html'), self.__dict__):
168 self.__setattr__(key, '')
170 book = super(Book, self).save(force_insert, force_update)
173 print self.mp3_file, self.mp3_file.path
174 extra_info = self.get_extra_info_value()
175 extra_info.update(self.get_mp3_info())
176 self.set_extra_info_value(extra_info)
177 book = super(Book, self).save(force_insert, force_update)
181 def get_mp3_info(self):
182 """Retrieves artist and director names from audio ID3 tags."""
183 audio = id3.ID3(self.mp3_file.path)
184 artist_name = ', '.join(', '.join(tag.text) for tag in audio.getall('TPE1'))
185 director_name = ', '.join(', '.join(tag.text) for tag in audio.getall('TPE3'))
186 return {'artist_name': artist_name, 'director_name': director_name}
188 def has_description(self):
189 return len(self.description) > 0
190 has_description.short_description = _('description')
191 has_description.boolean = True
193 def has_pdf_file(self):
194 return bool(self.pdf_file)
195 has_pdf_file.short_description = 'PDF'
196 has_pdf_file.boolean = True
198 def has_odt_file(self):
199 return bool(self.odt_file)
200 has_odt_file.short_description = 'ODT'
201 has_odt_file.boolean = True
203 def has_html_file(self):
204 return bool(self.html_file)
205 has_html_file.short_description = 'HTML'
206 has_html_file.boolean = True
208 class AlreadyExists(Exception):
212 def from_xml_file(xml_file, overwrite=False):
213 from tempfile import NamedTemporaryFile
214 from slughifi import slughifi
215 from markupstring import MarkupString
218 book_info = dcparser.parse(xml_file)
219 book_base, book_slug = book_info.url.rsplit('/', 1)
220 book, created = Book.objects.get_or_create(slug=book_slug)
226 raise Book.AlreadyExists(_('Book %s already exists') % book_slug)
227 # Save shelves for this book
228 book_shelves = list(book.tags.filter(category='set'))
230 book.title = book_info.title
231 book.set_extra_info_value(book_info.to_dict())
232 book._short_html = ''
236 for category in ('kind', 'genre', 'author', 'epoch'):
237 tag_name = getattr(book_info, category)
238 tag_sort_key = tag_name
239 if category == 'author':
240 tag_sort_key = tag_name.last_name
241 tag_name = ' '.join(tag_name.first_names) + ' ' + tag_name.last_name
242 tag, created = Tag.objects.get_or_create(slug=slughifi(tag_name))
245 tag.sort_key = slughifi(tag_sort_key)
246 tag.category = category
248 book_tags.append(tag)
250 book_tag, created = Tag.objects.get_or_create(slug=('l-' + book.slug)[:120])
252 book_tag.name = book.title[:50]
253 book_tag.sort_key = ('l-' + book.slug)[:120]
254 book_tag.category = 'book'
256 book_tags.append(book_tag)
258 book.tags = book_tags
260 if hasattr(book_info, 'parts'):
261 for n, part_url in enumerate(book_info.parts):
262 base, slug = part_url.rsplit('/', 1)
264 child_book = Book.objects.get(slug=slug)
265 child_book.parent = book
266 child_book.parent_number = n
268 except Book.DoesNotExist, e:
269 raise Book.DoesNotExist(_('Book with slug = "%s" does not exist.') % slug)
271 book_descendants = list(book.children.all())
272 while len(book_descendants) > 0:
273 child_book = book_descendants.pop(0)
274 for fragment in child_book.fragments.all():
275 fragment.tags = set(list(fragment.tags) + [book_tag])
276 book_descendants += list(child_book.children.all())
278 # Save XML and HTML files
279 if not isinstance(xml_file, File):
280 xml_file = File(file(xml_file))
281 book.xml_file.save('%s.xml' % book.slug, xml_file, save=False)
283 html_file = NamedTemporaryFile()
284 if html.transform(book.xml_file.path, html_file):
285 book.html_file.save('%s.html' % book.slug, File(html_file), save=False)
288 closed_fragments, open_fragments = html.extract_fragments(book.html_file.path)
290 for fragment in closed_fragments.values():
291 text = fragment.to_string()
293 if (len(MarkupString(text)) > 240):
294 short_text = unicode(MarkupString(text)[:160])
295 new_fragment, created = Fragment.objects.get_or_create(anchor=fragment.id, book=book,
296 defaults={'text': text, 'short_text': short_text})
299 theme_names = [s.strip() for s in fragment.themes.split(',')]
300 except AttributeError:
303 for theme_name in theme_names:
304 tag, created = Tag.objects.get_or_create(slug=slughifi(theme_name))
306 tag.name = theme_name
307 tag.sort_key = slughifi(theme_name)
308 tag.category = 'theme'
312 new_fragment.tags = set(list(book.tags) + themes + [book_tag])
313 book_themes += themes
315 book_themes = set(book_themes)
316 book.tags = list(book.tags) + list(book_themes) + book_shelves
322 def get_absolute_url(self):
323 return ('catalogue.views.book_detail', [self.slug])
326 ordering = ('title',)
327 verbose_name = _('book')
328 verbose_name_plural = _('books')
330 def __unicode__(self):
334 class Fragment(models.Model):
335 text = models.TextField()
336 short_text = models.TextField(editable=False)
337 _short_html = models.TextField(editable=False)
338 anchor = models.CharField(max_length=120)
339 book = models.ForeignKey(Book, related_name='fragments')
341 objects = models.Manager()
342 tagged = managers.ModelTaggedItemManager(Tag)
343 tags = managers.TagDescriptor(Tag)
345 def short_html(self):
346 key = '_short_html_%s' % get_language()
347 short_html = getattr(self, key)
348 if short_html and len(short_html):
349 return mark_safe(short_html)
351 book_authors = [mark_safe(u'<a href="%s">%s</a>' % (tag.get_absolute_url(), tag.name))
352 for tag in self.book.tags if tag.category == 'author']
354 setattr(self, key, unicode(render_to_string('catalogue/fragment_short.html',
355 {'fragment': self, 'book': self.book, 'book_authors': book_authors})))
357 return mark_safe(getattr(self, key))
359 def get_absolute_url(self):
360 return '%s#m%s' % (reverse('book_text', kwargs={'slug': self.book.slug}), self.anchor)
363 ordering = ('book', 'anchor',)
364 verbose_name = _('fragment')
365 verbose_name_plural = _('fragments')
368 class BookStub(models.Model):
369 title = models.CharField(_('title'), max_length=120)
370 author = models.CharField(_('author'), max_length=120)
371 pd = models.IntegerField(_('goes to public domain'), null=True, blank=True)
372 slug = models.SlugField(_('slug'), max_length=120, unique=True, db_index=True)
373 translator = models.TextField(_('translator'), blank=True)
374 translator_death = models.TextField(_('year of translator\'s death'), blank=True)
377 return self.pd is not None and self.pd <= datetime.now().year
384 def get_absolute_url(self):
385 return ('catalogue.views.book_detail', [self.slug])
387 def __unicode__(self):
391 ordering = ('title',)
392 verbose_name = _('book stub')
393 verbose_name_plural = _('book stubs')