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