Added migration to add extra info to book.
[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     
98     parent = models.ForeignKey('self', blank=True, null=True, related_name='children')
99     
100     objects = models.Manager()
101     tagged = managers.ModelTaggedItemManager(Tag)
102     tags = managers.TagDescriptor(Tag)
103
104     
105     @property
106     def name(self):
107         return self.title
108     
109     def short_html(self):
110         if len(self._short_html):
111             return mark_safe(self._short_html)
112         else:
113             tags = self.tags.filter(~Q(category__in=('set', 'theme', 'book')))
114             tags = [u'<a href="%s">%s</a>' % (tag.get_absolute_url(), tag.name) for tag in tags]
115
116             formats = []
117             if self.html_file:
118                 formats.append(u'<a href="%s">Czytaj online</a>' % reverse('book_text', kwargs={'slug': self.slug}))
119             if self.pdf_file:
120                 formats.append(u'<a href="%s">Plik PDF</a>' % self.pdf_file.url)
121             if self.odt_file:
122                 formats.append(u'<a href="%s">Plik ODT</a>' % self.odt_file.url)
123             if self.txt_file:
124                 formats.append(u'<a href="%s">Plik TXT</a>' % self.txt_file.url)
125             
126             self._short_html = unicode(render_to_string('catalogue/book_short.html',
127                 {'book': self, 'tags': tags, 'formats': formats}))
128             self.save()
129             return mark_safe(self._short_html)
130     
131     def has_description(self):
132         return len(self.description) > 0
133     has_description.short_description = _('description')
134     has_description.boolean = True
135     
136     def has_pdf_file(self):
137         return bool(self.pdf_file)
138     has_pdf_file.short_description = 'PDF'
139     has_pdf_file.boolean = True
140     
141     def has_odt_file(self):
142         return bool(self.odt_file)
143     has_odt_file.short_description = 'ODT'
144     has_odt_file.boolean = True
145     
146     def has_html_file(self):
147         return bool(self.html_file)
148     has_html_file.short_description = 'HTML'
149     has_html_file.boolean = True
150
151     class AlreadyExists(Exception):
152         pass
153     
154     @staticmethod
155     def from_xml_file(xml_file, overwrite=False):
156         from tempfile import NamedTemporaryFile
157         from slughifi import slughifi
158         from markupstring import MarkupString
159         
160         # Read book metadata
161         book_info = dcparser.parse(xml_file)
162         book_base, book_slug = book_info.url.rsplit('/', 1)
163         book, created = Book.objects.get_or_create(slug=book_slug)
164         
165         if created:
166             book_shelves = []
167         else:
168             if not overwrite:
169                 raise Book.AlreadyExists('Book %s already exists' % book_slug)
170             # Save shelves for this book
171             book_shelves = list(book.tags.filter(category='set'))
172         
173         book.title = book_info.title
174         book.extra_info = book_info.to_dict()
175         book._short_html = ''
176         book.save()
177         
178         book_tags = []
179         for category in ('kind', 'genre', 'author', 'epoch'):    
180             tag_name = getattr(book_info, category)
181             tag_sort_key = tag_name
182             if category == 'author':
183                 tag_sort_key = tag_name.last_name
184                 tag_name = ' '.join(tag_name.first_names) + ' ' + tag_name.last_name
185             tag, created = Tag.objects.get_or_create(slug=slughifi(tag_name))
186             if created:
187                 tag.name = tag_name
188                 tag.sort_key = slughifi(tag_sort_key)
189                 tag.category = category
190                 tag.save()
191             book_tags.append(tag)
192             
193         book_tag, created = Tag.objects.get_or_create(slug=('l-' + book.slug)[:120])
194         if created:
195             book_tag.name = book.title[:50]
196             book_tag.sort_key = ('l-' + book.slug)[:120]
197             book_tag.category = 'book'
198             book_tag.save()
199         book_tags.append(book_tag)
200         
201         book.tags = book_tags
202         
203         if hasattr(book_info, 'parts'):
204             for n, part_url in enumerate(book_info.parts):
205                 base, slug = part_url.rsplit('/', 1)
206                 child_book = Book.objects.get(slug=slug)
207                 child_book.parent = book
208                 child_book.parent_number = n
209                 child_book.save()
210
211         book_descendants = list(book.children.all())
212         while len(book_descendants) > 0:
213             child_book = book_descendants.pop(0)
214             for fragment in child_book.fragments.all():
215                 fragment.tags = set(list(fragment.tags) + [book_tag])
216             book_descendants += list(child_book.children.all())
217             
218         # Save XML and HTML files
219         book.xml_file.save('%s.xml' % book.slug, File(file(xml_file)), save=False)
220         
221         html_file = NamedTemporaryFile()
222         if html.transform(book.xml_file.path, html_file):
223             book.html_file.save('%s.html' % book.slug, File(html_file), save=False)
224             
225             # Extract fragments
226             closed_fragments, open_fragments = html.extract_fragments(book.html_file.path)
227             book_themes = []
228             for fragment in closed_fragments.values():
229                 text = fragment.to_string()
230                 short_text = ''
231                 if (len(MarkupString(text)) > 240):
232                     short_text = unicode(MarkupString(text)[:160])
233                 new_fragment, created = Fragment.objects.get_or_create(anchor=fragment.id, book=book, 
234                     defaults={'text': text, 'short_text': short_text})
235                 
236                 try:
237                     theme_names = [s.strip() for s in fragment.themes.split(',')]
238                 except AttributeError:
239                     continue
240                 themes = []
241                 for theme_name in theme_names:
242                     tag, created = Tag.objects.get_or_create(slug=slughifi(theme_name))
243                     if created:
244                         tag.name = theme_name
245                         tag.sort_key = slughifi(theme_name)
246                         tag.category = 'theme'
247                         tag.save()
248                     themes.append(tag)
249                 new_fragment.save()
250                 new_fragment.tags = set(list(book.tags) + themes + [book_tag])
251                 book_themes += themes
252             
253             book_themes = set(book_themes)
254             book.tags = list(book.tags) + list(book_themes) + book_shelves
255         
256         book.save()
257         return book
258     
259     @permalink
260     def get_absolute_url(self):
261         return ('catalogue.views.book_detail', [self.slug])
262         
263     class Meta:
264         ordering = ('title',)
265         verbose_name = _('book')
266         verbose_name_plural = _('books')
267
268     def __unicode__(self):
269         return self.title
270
271
272 class Fragment(models.Model):
273     text = models.TextField()
274     short_text = models.TextField(editable=False)
275     _short_html = models.TextField(editable=False)
276     anchor = models.CharField(max_length=120)
277     book = models.ForeignKey(Book, related_name='fragments')
278
279     objects = models.Manager()
280     tagged = managers.ModelTaggedItemManager(Tag)
281     tags = managers.TagDescriptor(Tag)
282     
283     def short_html(self):
284         if len(self._short_html):
285             return mark_safe(self._short_html)
286         else:
287             book_authors = [u'<a href="%s">%s</a>' % (tag.get_absolute_url(), tag.name) 
288                 for tag in self.book.tags if tag.category == 'author']
289             
290             self._short_html = unicode(render_to_string('catalogue/fragment_short.html',
291                 {'fragment': self, 'book': self.book, 'book_authors': book_authors}))
292             self.save()
293             return mark_safe(self._short_html)
294     
295     def get_absolute_url(self):
296         return '%s#m%s' % (reverse('book_text', kwargs={'slug': self.book.slug}), self.anchor)
297     
298     class Meta:
299         ordering = ('book', 'anchor',)
300         verbose_name = _('fragment')
301         verbose_name_plural = _('fragments')
302