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 datetime import datetime
7 from django.db import models
8 from django.db.models import permalink, Q
10 from django.core.cache import cache
11 from django.utils.translation import ugettext_lazy as _
12 from django.contrib.auth.models import User
13 from django.core.files import File
14 from django.template.loader import render_to_string
15 from django.utils.safestring import mark_safe
16 from django.utils.translation import get_language
17 from django.core.urlresolvers import reverse
18 from django.db.models.signals import post_save, m2m_changed, pre_delete
20 from django.conf import settings
22 from newtagging.models import TagBase, tags_updated
23 from newtagging import managers
24 from catalogue.fields import JSONField, OverwritingFileField
25 from catalogue.utils import ExistingFile
27 from librarian import dcparser, html, epub, NoDublinCore
29 from mutagen import id3
30 from slughifi import slughifi
31 from sortify import sortify
35 ('author', _('author')),
36 ('epoch', _('epoch')),
38 ('genre', _('genre')),
39 ('theme', _('theme')),
45 ('odt', _('ODT file')),
46 ('mp3', _('MP3 file')),
47 ('ogg', _('OGG file')),
48 ('daisy', _('DAISY file')),
51 # not quite, but Django wants you to set a timeout
52 CACHE_FOREVER = 2419200 # 28 days
54 class TagSubcategoryManager(models.Manager):
55 def __init__(self, subcategory):
56 super(TagSubcategoryManager, self).__init__()
57 self.subcategory = subcategory
59 def get_query_set(self):
60 return super(TagSubcategoryManager, self).get_query_set().filter(category=self.subcategory)
64 name = models.CharField(_('name'), max_length=50, db_index=True)
65 slug = models.SlugField(_('slug'), max_length=120, db_index=True)
66 sort_key = models.CharField(_('sort key'), max_length=120, db_index=True)
67 category = models.CharField(_('category'), max_length=50, blank=False, null=False,
68 db_index=True, choices=TAG_CATEGORIES)
69 description = models.TextField(_('description'), blank=True)
70 main_page = models.BooleanField(_('main page'), default=False, db_index=True, help_text=_('Show tag on main page'))
72 user = models.ForeignKey(User, blank=True, null=True)
73 book_count = models.IntegerField(_('book count'), blank=True, null=True)
74 gazeta_link = models.CharField(blank=True, max_length=240)
75 wiki_link = models.CharField(blank=True, max_length=240)
77 created_at = models.DateTimeField(_('creation date'), auto_now_add=True, db_index=True)
78 changed_at = models.DateTimeField(_('creation date'), auto_now=True, db_index=True)
80 class UrlDeprecationWarning(DeprecationWarning):
91 categories_dict = dict((item[::-1] for item in categories_rev.iteritems()))
94 ordering = ('sort_key',)
95 verbose_name = _('tag')
96 verbose_name_plural = _('tags')
97 unique_together = (("slug", "category"),)
99 def __unicode__(self):
103 return "Tag(slug=%r)" % self.slug
106 def get_absolute_url(self):
107 return ('catalogue.views.tagged_object_list', [self.url_chunk])
109 def has_description(self):
110 return len(self.description) > 0
111 has_description.short_description = _('description')
112 has_description.boolean = True
115 """ returns global book count for book tags, fragment count for themes """
117 if self.book_count is None:
118 if self.category == 'book':
120 objects = Book.objects.none()
121 elif self.category == 'theme':
122 objects = Fragment.tagged.with_all((self,))
124 objects = Book.tagged.with_all((self,)).order_by()
125 if self.category != 'set':
126 # eliminate descendants
127 l_tags = Tag.objects.filter(slug__in=[book.book_tag_slug() for book in objects])
128 descendants_keys = [book.pk for book in Book.tagged.with_any(l_tags)]
130 objects = objects.exclude(pk__in=descendants_keys)
131 self.book_count = objects.count()
133 return self.book_count
136 def get_tag_list(tags):
137 if isinstance(tags, basestring):
142 tags_splitted = tags.split('/')
143 for name in tags_splitted:
145 real_tags.append(Tag.objects.get(slug=name, category=category))
147 elif name in Tag.categories_rev:
148 category = Tag.categories_rev[name]
151 real_tags.append(Tag.objects.exclude(category='book').get(slug=name))
153 except Tag.MultipleObjectsReturned, e:
154 ambiguous_slugs.append(name)
157 # something strange left off
158 raise Tag.DoesNotExist()
160 # some tags should be qualified
161 e = Tag.MultipleObjectsReturned()
163 e.ambiguous_slugs = ambiguous_slugs
166 e = Tag.UrlDeprecationWarning()
171 return TagBase.get_tag_list(tags)
175 return '/'.join((Tag.categories_dict[self.category], self.slug))
178 # TODO: why is this hard-coded ?
179 def book_upload_path(ext=None, maxlen=100):
180 def get_dynamic_path(media, filename, ext=ext):
181 # how to put related book's slug here?
183 if media.type == 'daisy':
188 name = slughifi(filename.split(".")[0])
190 name = slughifi(media.name)
191 return 'book/%s/%s.%s' % (ext, name[:maxlen-len('book/%s/.%s' % (ext, ext))-4], ext)
192 return get_dynamic_path
195 class BookMedia(models.Model):
196 type = models.CharField(_('type'), choices=MEDIA_FORMATS, max_length="100")
197 name = models.CharField(_('name'), max_length="100")
198 file = OverwritingFileField(_('file'), upload_to=book_upload_path())
199 uploaded_at = models.DateTimeField(_('creation date'), auto_now_add=True, editable=False)
200 extra_info = JSONField(_('extra information'), default='{}', editable=False)
201 book = models.ForeignKey('Book', related_name='media')
202 source_sha1 = models.CharField(null=True, blank=True, max_length=40, editable=False)
204 def __unicode__(self):
205 return "%s (%s)" % (self.name, self.file.name.split("/")[-1])
208 ordering = ('type', 'name')
209 verbose_name = _('book media')
210 verbose_name_plural = _('book media')
212 def save(self, *args, **kwargs):
214 old = BookMedia.objects.get(pk=self.pk)
215 except BookMedia.DoesNotExist, e:
218 # if name changed, change the file name, too
219 if slughifi(self.name) != slughifi(old.name):
220 self.file.save(None, ExistingFile(self.file.path), save=False, leave=True)
222 super(BookMedia, self).save(*args, **kwargs)
223 extra_info = self.get_extra_info_value()
224 extra_info.update(self.read_meta())
225 self.set_extra_info_value(extra_info)
226 self.source_sha1 = self.read_source_sha1(self.file.path, self.type)
227 return super(BookMedia, self).save(*args, **kwargs)
231 Reads some metadata from the audiobook.
234 artist_name = director_name = project = funded_by = ''
235 if self.type == 'mp3':
237 audio = id3.ID3(self.file.path)
238 artist_name = ', '.join(', '.join(tag.text) for tag in audio.getall('TPE1'))
239 director_name = ', '.join(', '.join(tag.text) for tag in audio.getall('TPE3'))
240 project = ", ".join([t.data for t in audio.getall('PRIV')
241 if t.owner=='wolnelektury.pl?project'])
242 funded_by = ", ".join([t.data for t in audio.getall('PRIV')
243 if t.owner=='wolnelektury.pl?funded_by'])
246 elif self.type == 'ogg':
248 audio = mutagen.File(self.file.path)
249 artist_name = ', '.join(audio.get('artist', []))
250 director_name = ', '.join(audio.get('conductor', []))
251 project = ", ".join(audio.get('project', []))
252 funded_by = ", ".join(audio.get('funded_by', []))
257 return {'artist_name': artist_name, 'director_name': director_name,
258 'project': project, 'funded_by': funded_by}
261 def read_source_sha1(filepath, filetype):
263 Reads source file SHA1 from audiobok metadata.
266 if filetype == 'mp3':
268 audio = id3.ID3(filepath)
269 return [t.data for t in audio.getall('PRIV')
270 if t.owner=='wolnelektury.pl?flac_sha1'][0]
273 elif filetype == 'ogg':
275 audio = mutagen.File(filepath)
276 return audio.get('flac_sha1', [None])[0]
283 class Book(models.Model):
284 title = models.CharField(_('title'), max_length=120)
285 sort_key = models.CharField(_('sort_key'), max_length=120, db_index=True, editable=False)
286 slug = models.SlugField(_('slug'), max_length=120, unique=True, db_index=True)
287 description = models.TextField(_('description'), blank=True)
288 created_at = models.DateTimeField(_('creation date'), auto_now_add=True, db_index=True)
289 changed_at = models.DateTimeField(_('creation date'), auto_now=True, db_index=True)
290 parent_number = models.IntegerField(_('parent number'), default=0)
291 extra_info = JSONField(_('extra information'), default='{}')
292 gazeta_link = models.CharField(blank=True, max_length=240)
293 wiki_link = models.CharField(blank=True, max_length=240)
294 # files generated during publication
295 xml_file = models.FileField(_('XML file'), upload_to=book_upload_path('xml'), blank=True)
296 html_file = models.FileField(_('HTML file'), upload_to=book_upload_path('html'), blank=True)
297 pdf_file = models.FileField(_('PDF file'), upload_to=book_upload_path('pdf'), blank=True)
298 epub_file = models.FileField(_('EPUB file'), upload_to=book_upload_path('epub'), blank=True)
299 txt_file = models.FileField(_('TXT file'), upload_to=book_upload_path('txt'), blank=True)
301 parent = models.ForeignKey('self', blank=True, null=True, related_name='children')
302 objects = models.Manager()
303 tagged = managers.ModelTaggedItemManager(Tag)
304 tags = managers.TagDescriptor(Tag)
306 html_built = django.dispatch.Signal()
308 class AlreadyExists(Exception):
312 ordering = ('sort_key',)
313 verbose_name = _('book')
314 verbose_name_plural = _('books')
316 def __unicode__(self):
319 def save(self, force_insert=False, force_update=False, reset_short_html=True, **kwargs):
320 self.sort_key = sortify(self.title)
322 ret = super(Book, self).save(force_insert, force_update)
325 self.reset_short_html()
330 def get_absolute_url(self):
331 return ('catalogue.views.book_detail', [self.slug])
337 def book_tag_slug(self):
338 return ('l-' + self.slug)[:120]
341 slug = self.book_tag_slug()
342 book_tag, created = Tag.objects.get_or_create(slug=slug, category='book')
344 book_tag.name = self.title[:50]
345 book_tag.sort_key = self.title.lower()
349 def has_media(self, type):
376 if self.media.filter(type=type).exists():
381 def get_media(self, type):
382 if self.has_media(type):
386 return self.html_file
388 return self.epub_file
394 return self.media.filter(type=type)
399 return self.get_media("mp3")
401 return self.get_media("odt")
403 return self.get_media("ogg")
405 return self.get_media("daisy")
407 def reset_short_html(self):
411 cache_key = "Book.short_html/%d/%s"
412 for lang, langname in settings.LANGUAGES:
413 cache.delete(cache_key % (self.id, lang))
414 # Fragment.short_html relies on book's tags, so reset it here too
415 for fragm in self.fragments.all():
416 fragm.reset_short_html()
418 def short_html(self):
420 cache_key = "Book.short_html/%d/%s" % (self.id, get_language())
421 short_html = cache.get(cache_key)
425 if short_html is not None:
426 return mark_safe(short_html)
428 tags = self.tags.filter(~Q(category__in=('set', 'theme', 'book')))
429 tags = [mark_safe(u'<a href="%s">%s</a>' % (tag.get_absolute_url(), tag.name)) for tag in tags]
432 # files generated during publication
433 if self.has_media("html"):
434 formats.append(u'<a href="%s">%s</a>' % (reverse('book_text', kwargs={'slug': self.slug}), _('Read online')))
435 if self.has_media("pdf"):
436 formats.append(u'<a href="%s">PDF</a>' % self.get_media('pdf').url)
437 if self.root_ancestor.has_media("epub"):
438 formats.append(u'<a href="%s">EPUB</a>' % self.root_ancestor.get_media('epub').url)
439 if self.has_media("txt"):
440 formats.append(u'<a href="%s">TXT</a>' % self.get_media('txt').url)
442 for m in self.media.order_by('type'):
443 formats.append(u'<a href="%s">%s</a>' % (m.file.url, m.type.upper()))
445 formats = [mark_safe(format) for format in formats]
447 short_html = unicode(render_to_string('catalogue/book_short.html',
448 {'book': self, 'tags': tags, 'formats': formats}))
451 cache.set(cache_key, short_html, CACHE_FOREVER)
452 return mark_safe(short_html)
455 def root_ancestor(self):
456 """ returns the oldest ancestor """
458 if not hasattr(self, '_root_ancestor'):
462 self._root_ancestor = book
463 return self._root_ancestor
466 def has_description(self):
467 return len(self.description) > 0
468 has_description.short_description = _('description')
469 has_description.boolean = True
472 def has_pdf_file(self):
473 return bool(self.pdf_file)
474 has_pdf_file.short_description = 'PDF'
475 has_pdf_file.boolean = True
477 def has_epub_file(self):
478 return bool(self.epub_file)
479 has_epub_file.short_description = 'EPUB'
480 has_epub_file.boolean = True
482 def has_txt_file(self):
483 return bool(self.txt_file)
484 has_txt_file.short_description = 'HTML'
485 has_txt_file.boolean = True
487 def has_html_file(self):
488 return bool(self.html_file)
489 has_html_file.short_description = 'HTML'
490 has_html_file.boolean = True
492 def has_odt_file(self):
493 return bool(self.has_media("odt"))
494 has_odt_file.short_description = 'ODT'
495 has_odt_file.boolean = True
497 def has_mp3_file(self):
498 return bool(self.has_media("mp3"))
499 has_mp3_file.short_description = 'MP3'
500 has_mp3_file.boolean = True
502 def has_ogg_file(self):
503 return bool(self.has_media("ogg"))
504 has_ogg_file.short_description = 'OGG'
505 has_ogg_file.boolean = True
507 def has_daisy_file(self):
508 return bool(self.has_media("daisy"))
509 has_daisy_file.short_description = 'DAISY'
510 has_daisy_file.boolean = True
512 def build_epub(self, remove_descendants=True):
513 """ (Re)builds the epub file.
514 If book has a parent, does nothing.
515 Unless remove_descendants is False, descendants' epubs are removed.
518 from StringIO import StringIO
519 from hashlib import sha1
520 from django.core.files.base import ContentFile
521 from librarian import DocProvider
523 class BookImportDocProvider(DocProvider):
524 """ used for joined EPUBs """
526 def __init__(self, book):
529 def by_slug(self, slug):
530 if slug == self.book.slug:
531 return self.book.xml_file
533 return Book.objects.get(slug=slug).xml_file
539 epub_file = StringIO()
541 epub.transform(BookImportDocProvider(self), self.slug, output_file=epub_file)
542 self.epub_file.save('%s.epub' % self.slug, ContentFile(epub_file.getvalue()))
543 FileRecord(slug=self.slug, type='epub', sha1=sha1(epub_file.getvalue()).hexdigest()).save()
547 book_descendants = list(self.children.all())
548 while len(book_descendants) > 0:
549 child_book = book_descendants.pop(0)
550 if remove_descendants and child_book.has_epub_file():
551 child_book.epub_file.delete()
552 # save anyway, to refresh short_html
554 book_descendants += list(child_book.children.all())
557 from StringIO import StringIO
558 from django.core.files.base import ContentFile
559 from librarian import text
562 text.transform(open(self.xml_file.path), out)
563 self.txt_file.save('%s.txt' % self.slug, ContentFile(out.getvalue()))
566 def build_html(self):
567 from tempfile import NamedTemporaryFile
568 from markupstring import MarkupString
570 meta_tags = list(self.tags.filter(
571 category__in=('author', 'epoch', 'genre', 'kind')))
572 book_tag = self.book_tag()
574 html_file = NamedTemporaryFile()
575 if html.transform(self.xml_file.path, html_file, parse_dublincore=False):
576 self.html_file.save('%s.html' % self.slug, File(html_file))
578 # get ancestor l-tags for adding to new fragments
582 ancestor_tags.append(p.book_tag())
585 # Delete old fragments and create them from scratch
586 self.fragments.all().delete()
588 closed_fragments, open_fragments = html.extract_fragments(self.html_file.path)
589 for fragment in closed_fragments.values():
591 theme_names = [s.strip() for s in fragment.themes.split(',')]
592 except AttributeError:
595 for theme_name in theme_names:
598 tag, created = Tag.objects.get_or_create(slug=slughifi(theme_name), category='theme')
600 tag.name = theme_name
601 tag.sort_key = theme_name.lower()
607 text = fragment.to_string()
609 if (len(MarkupString(text)) > 240):
610 short_text = unicode(MarkupString(text)[:160])
611 new_fragment = Fragment.objects.create(anchor=fragment.id, book=self,
612 text=text, short_text=short_text)
615 new_fragment.tags = set(meta_tags + themes + [book_tag] + ancestor_tags)
617 self.html_built.send(sender=self)
623 def from_xml_file(cls, xml_file, **kwargs):
624 # use librarian to parse meta-data
625 book_info = dcparser.parse(xml_file)
627 if not isinstance(xml_file, File):
628 xml_file = File(open(xml_file))
631 return cls.from_text_and_meta(xml_file, book_info, **kwargs)
636 def from_text_and_meta(cls, raw_file, book_info, overwrite=False, build_epub=True, build_txt=True):
639 # check for parts before we do anything
641 if hasattr(book_info, 'parts'):
642 for part_url in book_info.parts:
643 base, slug = part_url.rsplit('/', 1)
645 children.append(Book.objects.get(slug=slug))
646 except Book.DoesNotExist, e:
647 raise Book.DoesNotExist(_('Book with slug = "%s" does not exist.') % slug)
651 book_base, book_slug = book_info.url.rsplit('/', 1)
652 if re.search(r'[^a-zA-Z0-9-]', book_slug):
653 raise ValueError('Invalid characters in slug')
654 book, created = Book.objects.get_or_create(slug=book_slug)
660 raise Book.AlreadyExists(_('Book %s already exists') % book_slug)
661 # Save shelves for this book
662 book_shelves = list(book.tags.filter(category='set'))
664 book.title = book_info.title
665 book.set_extra_info_value(book_info.to_dict())
669 categories = (('kinds', 'kind'), ('genres', 'genre'), ('authors', 'author'), ('epochs', 'epoch'))
670 for field_name, category in categories:
672 tag_names = getattr(book_info, field_name)
674 tag_names = [getattr(book_info, category)]
675 for tag_name in tag_names:
676 tag_sort_key = tag_name
677 if category == 'author':
678 tag_sort_key = tag_name.last_name
679 tag_name = ' '.join(tag_name.first_names) + ' ' + tag_name.last_name
680 tag, created = Tag.objects.get_or_create(slug=slughifi(tag_name), category=category)
683 tag.sort_key = sortify(tag_sort_key.lower())
685 meta_tags.append(tag)
687 book.tags = set(meta_tags + book_shelves)
689 book_tag = book.book_tag()
691 for n, child_book in enumerate(children):
692 child_book.parent = book
693 child_book.parent_number = n
696 # Save XML and HTML files
697 book.xml_file.save('%s.xml' % book.slug, raw_file, save=False)
699 # delete old fragments when overwriting
700 book.fragments.all().delete()
702 if book.build_html():
703 if not settings.NO_BUILD_TXT and build_txt:
706 if not settings.NO_BUILD_EPUB and build_epub:
707 book.root_ancestor.build_epub()
709 book_descendants = list(book.children.all())
710 # add l-tag to descendants and their fragments
711 # delete unnecessary EPUB files
712 while len(book_descendants) > 0:
713 child_book = book_descendants.pop(0)
714 child_book.tags = list(child_book.tags) + [book_tag]
716 for fragment in child_book.fragments.all():
717 fragment.tags = set(list(fragment.tags) + [book_tag])
718 book_descendants += list(child_book.children.all())
723 book.reset_tag_counter()
724 book.reset_theme_counter()
728 def reset_tag_counter(self):
732 cache_key = "Book.tag_counter/%d" % self.id
733 cache.delete(cache_key)
735 self.parent.reset_tag_counter()
738 def tag_counter(self):
740 cache_key = "Book.tag_counter/%d" % self.id
741 tags = cache.get(cache_key)
747 for child in self.children.all().order_by():
748 for tag_pk, value in child.tag_counter.iteritems():
749 tags[tag_pk] = tags.get(tag_pk, 0) + value
750 for tag in self.tags.exclude(category__in=('book', 'theme', 'set')).order_by():
754 cache.set(cache_key, tags, CACHE_FOREVER)
757 def reset_theme_counter(self):
761 cache_key = "Book.theme_counter/%d" % self.id
762 cache.delete(cache_key)
764 self.parent.reset_theme_counter()
767 def theme_counter(self):
769 cache_key = "Book.theme_counter/%d" % self.id
770 tags = cache.get(cache_key)
776 for fragment in Fragment.tagged.with_any([self.book_tag()]).order_by():
777 for tag in fragment.tags.filter(category='theme').order_by():
778 tags[tag.pk] = tags.get(tag.pk, 0) + 1
781 cache.set(cache_key, tags, CACHE_FOREVER)
784 def pretty_title(self, html_links=False):
786 names = list(book.tags.filter(category='author'))
792 names.extend(reversed(books))
795 names = ['<a href="%s">%s</a>' % (tag.get_absolute_url(), tag.name) for tag in names]
797 names = [tag.name for tag in names]
799 return ', '.join(names)
802 def tagged_top_level(cls, tags):
803 """ Returns top-level books tagged with `tags'.
805 It only returns those books which don't have ancestors which are
806 also tagged with those tags.
809 # get relevant books and their tags
810 objects = cls.tagged.with_all(tags)
811 # eliminate descendants
812 l_tags = Tag.objects.filter(category='book', slug__in=[book.book_tag_slug() for book in objects])
813 descendants_keys = [book.pk for book in cls.tagged.with_any(l_tags)]
815 objects = objects.exclude(pk__in=descendants_keys)
820 class Fragment(models.Model):
821 text = models.TextField()
822 short_text = models.TextField(editable=False)
823 anchor = models.CharField(max_length=120)
824 book = models.ForeignKey(Book, related_name='fragments')
826 objects = models.Manager()
827 tagged = managers.ModelTaggedItemManager(Tag)
828 tags = managers.TagDescriptor(Tag)
831 ordering = ('book', 'anchor',)
832 verbose_name = _('fragment')
833 verbose_name_plural = _('fragments')
835 def get_absolute_url(self):
836 return '%s#m%s' % (reverse('book_text', kwargs={'slug': self.book.slug}), self.anchor)
838 def reset_short_html(self):
842 cache_key = "Fragment.short_html/%d/%s"
843 for lang, langname in settings.LANGUAGES:
844 cache.delete(cache_key % (self.id, lang))
846 def short_html(self):
848 cache_key = "Fragment.short_html/%d/%s" % (self.id, get_language())
849 short_html = cache.get(cache_key)
853 if short_html is not None:
854 return mark_safe(short_html)
856 short_html = unicode(render_to_string('catalogue/fragment_short.html',
859 cache.set(cache_key, short_html, CACHE_FOREVER)
860 return mark_safe(short_html)
863 class FileRecord(models.Model):
864 slug = models.SlugField(_('slug'), max_length=120, db_index=True)
865 type = models.CharField(_('type'), max_length=20, db_index=True)
866 sha1 = models.CharField(_('sha-1 hash'), max_length=40)
867 time = models.DateTimeField(_('time'), auto_now_add=True)
870 ordering = ('-time','-slug', '-type')
871 verbose_name = _('file record')
872 verbose_name_plural = _('file records')
874 def __unicode__(self):
875 return "%s %s.%s" % (self.sha1, self.slug, self.type)
884 def _tags_updated_handler(sender, affected_tags, **kwargs):
885 # reset tag global counter
886 # we want Tag.changed_at updated for API to know the tag was touched
887 Tag.objects.filter(pk__in=[tag.pk for tag in affected_tags]).update(book_count=None, changed_at=datetime.now())
889 # if book tags changed, reset book tag counter
890 if isinstance(sender, Book) and \
891 Tag.objects.filter(pk__in=(tag.pk for tag in affected_tags)).\
892 exclude(category__in=('book', 'theme', 'set')).count():
893 sender.reset_tag_counter()
894 # if fragment theme changed, reset book theme counter
895 elif isinstance(sender, Fragment) and \
896 Tag.objects.filter(pk__in=(tag.pk for tag in affected_tags)).\
897 filter(category='theme').count():
898 sender.book.reset_theme_counter()
899 tags_updated.connect(_tags_updated_handler)
902 def _pre_delete_handler(sender, instance, **kwargs):
903 """ refresh Book on BookMedia delete """
904 if sender == BookMedia:
906 pre_delete.connect(_pre_delete_handler)
908 def _post_save_handler(sender, instance, **kwargs):
909 """ refresh all the short_html stuff on BookMedia update """
910 if sender == BookMedia:
912 post_save.connect(_post_save_handler)