0cd19cc0cf99d7d8bf1ceb0e49014d4a1ea8a367
[wolnelektury.git] / apps / catalogue / models.py
1 # -*- coding: utf-8 -*-
2 from django.db import models
3 from django.db.models import permalink, Q
4 from django.utils.translation import ugettext_lazy as _
5 from django.contrib.auth.models import User
6 from django.core.files import File
7 from django.template.loader import render_to_string
8 from django.utils.safestring import mark_safe
9 from django.core.urlresolvers import reverse
10
11 from newtagging.models import TagBase
12 from newtagging import managers
13 from catalogue.fields import JSONField
14
15 from librarian import html, dcparser
16 from mutagen import id3
17
18
19 TAG_CATEGORIES = (
20     ('author', _('author')),
21     ('epoch', _('epoch')),
22     ('kind', _('kind')),
23     ('genre', _('genre')),
24     ('theme', _('theme')),
25     ('set', _('set')),
26     ('book', _('book')),
27 )
28
29
30 class TagSubcategoryManager(models.Manager):
31     def __init__(self, subcategory):
32         super(TagSubcategoryManager, self).__init__()
33         self.subcategory = subcategory
34         
35     def get_query_set(self):
36         return super(TagSubcategoryManager, self).get_query_set().filter(category=self.subcategory)
37
38
39 class Tag(TagBase):
40     name = models.CharField(_('name'), max_length=50, db_index=True)
41     slug = models.SlugField(_('slug'), max_length=120, unique=True, db_index=True)
42     sort_key = models.SlugField(_('sort key'), max_length=120, db_index=True)
43     category = models.CharField(_('category'), max_length=50, blank=False, null=False, 
44         db_index=True, choices=TAG_CATEGORIES)
45     description = models.TextField(_('description'), blank=True)
46     main_page = models.BooleanField(_('main page'), default=False, db_index=True, help_text=_('Show tag on main page'))
47     
48     user = models.ForeignKey(User, blank=True, null=True)
49     book_count = models.IntegerField(_('book count'), default=0, blank=False, null=False)
50     
51     def has_description(self):
52         return len(self.description) > 0
53     has_description.short_description = _('description')
54     has_description.boolean = True
55
56     @permalink
57     def get_absolute_url(self):
58         return ('catalogue.views.tagged_object_list', [self.slug])
59     
60     class Meta:
61         ordering = ('sort_key',)
62         verbose_name = _('tag')
63         verbose_name_plural = _('tags')
64     
65     def __unicode__(self):
66         return self.name
67
68     @staticmethod
69     def get_tag_list(tags):
70         if isinstance(tags, basestring):
71             tag_slugs = tags.split('/')
72             return [Tag.objects.get(slug=slug) for slug in tag_slugs]
73         else:
74             return TagBase.get_tag_list(tags)
75
76
77 def book_upload_path(ext):
78     def get_dynamic_path(book, filename):
79         return 'lektura/%s.%s' % (book.slug, ext)
80     return get_dynamic_path
81
82
83 class Book(models.Model):
84     title = models.CharField(_('title'), max_length=120)
85     slug = models.SlugField(_('slug'), max_length=120, unique=True, db_index=True)
86     description = models.TextField(_('description'), blank=True)
87     created_at = models.DateTimeField(_('creation date'), auto_now=True)
88     _short_html = models.TextField(_('short HTML'), editable=False)
89     parent_number = models.IntegerField(_('parent number'), default=0)
90     extra_info = JSONField(_('extra information'))
91     
92     # Formats
93     xml_file = models.FileField(_('XML file'), upload_to=book_upload_path('xml'), blank=True)
94     html_file = models.FileField(_('HTML file'), upload_to=book_upload_path('html'), blank=True)
95     pdf_file = models.FileField(_('PDF file'), upload_to=book_upload_path('pdf'), blank=True)
96     odt_file = models.FileField(_('ODT file'), upload_to=book_upload_path('odt'), blank=True)
97     txt_file = models.FileField(_('TXT file'), upload_to=book_upload_path('txt'), blank=True)
98     mp3_file = models.FileField(_('MP3 file'), upload_to=book_upload_path('mp3'), blank=True)
99     ogg_file = models.FileField(_('OGG file'), upload_to=book_upload_path('ogg'), blank=True)
100     
101     parent = models.ForeignKey('self', blank=True, null=True, related_name='children')
102     
103     objects = models.Manager()
104     tagged = managers.ModelTaggedItemManager(Tag)
105     tags = managers.TagDescriptor(Tag)
106
107     
108     @property
109     def name(self):
110         return self.title
111     
112     def short_html(self):
113         if len(self._short_html):
114             return mark_safe(self._short_html)
115         else:
116             tags = self.tags.filter(~Q(category__in=('set', 'theme', 'book')))
117             tags = [u'<a href="%s">%s</a>' % (tag.get_absolute_url(), tag.name) for tag in tags]
118
119             formats = []
120             if self.html_file:
121                 formats.append(u'<a href="%s">Czytaj online</a>' % reverse('book_text', kwargs={'slug': self.slug}))
122             if self.pdf_file:
123                 formats.append(u'<a href="%s">PDF</a>' % self.pdf_file.url)
124             if self.odt_file:
125                 formats.append(u'<a href="%s">ODT</a>' % self.odt_file.url)
126             if self.txt_file:
127                 formats.append(u'<a href="%s">TXT</a>' % self.txt_file.url)
128             if self.mp3_file:
129                 formats.append(u'<a href="%s">MP3</a>' % self.mp3_file.url)
130             if self.ogg_file:
131                 formats.append(u'<a href="%s">OGG</a>' % self.ogg_file.url)
132             
133             self._short_html = unicode(render_to_string('catalogue/book_short.html',
134                 {'book': self, 'tags': tags, 'formats': formats}))
135             self.save()
136             return mark_safe(self._short_html)
137     
138     def save(self, force_insert=False, force_update=False):
139         if self.mp3_file:
140             extra_info = self.get_extra_info_value()
141             extra_info.update(self.get_mp3_info())
142             self.set_extra_info_value(extra_info)
143         return super(Book, self).save(force_insert, force_update)
144     
145     def get_mp3_info(self):
146         """Retrieves artist and director names from audio ID3 tags."""
147         audio = id3.ID3(self.mp3_file.path)
148         artist_name = ', '.join(', '.join(tag.text) for tag in audio.getall('TPE1'))
149         director_name = ', '.join(', '.join(tag.text) for tag in audio.getall('TPE3'))
150         return {'artist_name': artist_name, 'director_name': director_name}
151         
152     def has_description(self):
153         return len(self.description) > 0
154     has_description.short_description = _('description')
155     has_description.boolean = True
156     
157     def has_pdf_file(self):
158         return bool(self.pdf_file)
159     has_pdf_file.short_description = 'PDF'
160     has_pdf_file.boolean = True
161     
162     def has_odt_file(self):
163         return bool(self.odt_file)
164     has_odt_file.short_description = 'ODT'
165     has_odt_file.boolean = True
166     
167     def has_html_file(self):
168         return bool(self.html_file)
169     has_html_file.short_description = 'HTML'
170     has_html_file.boolean = True
171
172     class AlreadyExists(Exception):
173         pass
174     
175     @staticmethod
176     def from_xml_file(xml_file, overwrite=False):
177         from tempfile import NamedTemporaryFile
178         from slughifi import slughifi
179         from markupstring import MarkupString
180         
181         # Read book metadata
182         book_info = dcparser.parse(xml_file)
183         book_base, book_slug = book_info.url.rsplit('/', 1)
184         book, created = Book.objects.get_or_create(slug=book_slug)
185         
186         if created:
187             book_shelves = []
188         else:
189             if not overwrite:
190                 raise Book.AlreadyExists('Book %s already exists' % book_slug)
191             # Save shelves for this book
192             book_shelves = list(book.tags.filter(category='set'))
193         
194         book.title = book_info.title
195         book.set_extra_info_value(book_info.to_dict())
196         book._short_html = ''
197         book.save()
198         
199         book_tags = []
200         for category in ('kind', 'genre', 'author', 'epoch'):    
201             tag_name = getattr(book_info, category)
202             tag_sort_key = tag_name
203             if category == 'author':
204                 tag_sort_key = tag_name.last_name
205                 tag_name = ' '.join(tag_name.first_names) + ' ' + tag_name.last_name
206             tag, created = Tag.objects.get_or_create(slug=slughifi(tag_name))
207             if created:
208                 tag.name = tag_name
209                 tag.sort_key = slughifi(tag_sort_key)
210                 tag.category = category
211                 tag.save()
212             book_tags.append(tag)
213             
214         book_tag, created = Tag.objects.get_or_create(slug=('l-' + book.slug)[:120])
215         if created:
216             book_tag.name = book.title[:50]
217             book_tag.sort_key = ('l-' + book.slug)[:120]
218             book_tag.category = 'book'
219             book_tag.save()
220         book_tags.append(book_tag)
221         
222         book.tags = book_tags
223         
224         if hasattr(book_info, 'parts'):
225             for n, part_url in enumerate(book_info.parts):
226                 base, slug = part_url.rsplit('/', 1)
227                 child_book = Book.objects.get(slug=slug)
228                 child_book.parent = book
229                 child_book.parent_number = n
230                 child_book.save()
231
232         book_descendants = list(book.children.all())
233         while len(book_descendants) > 0:
234             child_book = book_descendants.pop(0)
235             for fragment in child_book.fragments.all():
236                 fragment.tags = set(list(fragment.tags) + [book_tag])
237             book_descendants += list(child_book.children.all())
238             
239         # Save XML and HTML files
240         if not isinstance(xml_file, File):
241             xml_file = File(file(xml_file))
242         book.xml_file.save('%s.xml' % book.slug, xml_file, save=False)
243         
244         html_file = NamedTemporaryFile()
245         if html.transform(book.xml_file.path, html_file):
246             book.html_file.save('%s.html' % book.slug, File(html_file), save=False)
247             
248             # Extract fragments
249             closed_fragments, open_fragments = html.extract_fragments(book.html_file.path)
250             book_themes = []
251             for fragment in closed_fragments.values():
252                 text = fragment.to_string()
253                 short_text = ''
254                 if (len(MarkupString(text)) > 240):
255                     short_text = unicode(MarkupString(text)[:160])
256                 new_fragment, created = Fragment.objects.get_or_create(anchor=fragment.id, book=book, 
257                     defaults={'text': text, 'short_text': short_text})
258                 
259                 try:
260                     theme_names = [s.strip() for s in fragment.themes.split(',')]
261                 except AttributeError:
262                     continue
263                 themes = []
264                 for theme_name in theme_names:
265                     tag, created = Tag.objects.get_or_create(slug=slughifi(theme_name))
266                     if created:
267                         tag.name = theme_name
268                         tag.sort_key = slughifi(theme_name)
269                         tag.category = 'theme'
270                         tag.save()
271                     themes.append(tag)
272                 new_fragment.save()
273                 new_fragment.tags = set(list(book.tags) + themes + [book_tag])
274                 book_themes += themes
275             
276             book_themes = set(book_themes)
277             book.tags = list(book.tags) + list(book_themes) + book_shelves
278         
279         book.save()
280         return book
281     
282     @permalink
283     def get_absolute_url(self):
284         return ('catalogue.views.book_detail', [self.slug])
285         
286     class Meta:
287         ordering = ('title',)
288         verbose_name = _('book')
289         verbose_name_plural = _('books')
290
291     def __unicode__(self):
292         return self.title
293
294
295 class Fragment(models.Model):
296     text = models.TextField()
297     short_text = models.TextField(editable=False)
298     _short_html = models.TextField(editable=False)
299     anchor = models.CharField(max_length=120)
300     book = models.ForeignKey(Book, related_name='fragments')
301
302     objects = models.Manager()
303     tagged = managers.ModelTaggedItemManager(Tag)
304     tags = managers.TagDescriptor(Tag)
305     
306     def short_html(self):
307         if len(self._short_html):
308             return mark_safe(self._short_html)
309         else:
310             book_authors = [u'<a href="%s">%s</a>' % (tag.get_absolute_url(), tag.name) 
311                 for tag in self.book.tags if tag.category == 'author']
312             
313             self._short_html = unicode(render_to_string('catalogue/fragment_short.html',
314                 {'fragment': self, 'book': self.book, 'book_authors': book_authors}))
315             self.save()
316             return mark_safe(self._short_html)
317     
318     def get_absolute_url(self):
319         return '%s#m%s' % (reverse('book_text', kwargs={'slug': self.book.slug}), self.anchor)
320     
321     class Meta:
322         ordering = ('book', 'anchor',)
323         verbose_name = _('fragment')
324         verbose_name_plural = _('fragments')
325