From: Marcin Koziej Date: Wed, 21 Dec 2011 09:03:56 +0000 (+0100) Subject: merge search into pretty branch X-Git-Url: https://git.mdrn.pl/wolnelektury.git/commitdiff_plain/f59e7c3de6bd0f85a61a4d9481db60cd7369ae92?hp=98a11b1afa8d82d4843705f6ffff799b1eeaed50 merge search into pretty branch --- diff --git a/apps/ajaxable/__init__.py b/apps/ajaxable/__init__.py new file mode 100644 index 000000000..a0105436b --- /dev/null +++ b/apps/ajaxable/__init__.py @@ -0,0 +1,4 @@ +""" +Provides a way to create forms behaving correctly as AJAX forms +as well as standalone forms without any Javascript. +""" diff --git a/apps/ajaxable/models.py b/apps/ajaxable/models.py new file mode 100644 index 000000000..e69de29bb diff --git a/apps/ajaxable/templates/ajaxable/form.html b/apps/ajaxable/templates/ajaxable/form.html new file mode 100755 index 000000000..d8f00361f --- /dev/null +++ b/apps/ajaxable/templates/ajaxable/form.html @@ -0,0 +1,9 @@ +{% load i18n %} +

{{ title }}

+ +
+
    + {{ form.as_ul }} +
  1. +
+
\ No newline at end of file diff --git a/apps/ajaxable/templates/ajaxable/form_on_page.html b/apps/ajaxable/templates/ajaxable/form_on_page.html new file mode 100755 index 000000000..61175d507 --- /dev/null +++ b/apps/ajaxable/templates/ajaxable/form_on_page.html @@ -0,0 +1,14 @@ +{% extends "base.html" %} +{% load i18n %} + +{% block titleextra %}{{ title }}{% endblock %} + +{% block body %} + + {% include ajax_template %} + + {% if response_data.message %} +

{{ response_data.message }}

+ {% endif %} + +{% endblock %} diff --git a/apps/ajaxable/utils.py b/apps/ajaxable/utils.py new file mode 100755 index 000000000..14b5dfc7a --- /dev/null +++ b/apps/ajaxable/utils.py @@ -0,0 +1,82 @@ +from django.http import HttpResponse, HttpResponseRedirect +from django.shortcuts import render_to_response +from django.template import RequestContext +from django.utils.encoding import force_unicode +from django.utils.functional import Promise +from django.utils.http import urlquote_plus +from django.utils import simplejson +from django.utils.translation import ugettext_lazy as _ + + +class LazyEncoder(simplejson.JSONEncoder): + def default(self, obj): + if isinstance(obj, Promise): + return force_unicode(obj) + return obj + +# shortcut for JSON reponses +class JSONResponse(HttpResponse): + def __init__(self, data={}, callback=None, **kwargs): + # get rid of mimetype + kwargs.pop('mimetype', None) + data = simplejson.dumps(data) + if callback: + data = callback + "(" + data + ");" + super(JSONResponse, self).__init__(data, mimetype="application/json", **kwargs) + + + +class AjaxableFormView(object): + """Subclass this to create an ajaxable view for any form. + + In the subclass, provide at least form_class. + + """ + form_class = None + # override to customize form look + template = "ajaxable/form.html" + # set to redirect after succesful ajax-less post + submit = _('Send') + redirect = None + title = '' + success_message = '' + formname = "form" + full_template = "ajaxable/form_on_page.html" + + def __call__(self, request): + """A view displaying a form, or JSON if `ajax' GET param is set.""" + ajax = request.GET.get('ajax', False) + if request.method == "POST": + form = self.form_class(data=request.POST) + if form.is_valid(): + self.success(form, request) + redirect = request.GET.get('next') + if not ajax and redirect is not None: + return HttpResponseRedirect(urlquote_plus( + redirect, safe='/?=')) + response_data = {'success': True, 'message': self.success_message} + else: + response_data = {'success': False, 'errors': form.errors} + if ajax: + return HttpResponse(LazyEncoder(ensure_ascii=False).encode(response_data)) + else: + form = self.form_class() + response_data = None + + template = self.template if ajax else self.full_template + return render_to_response(template, { + self.formname: form, + "title": self.title, + "submit": self.submit, + "response_data": response_data, + "ajax_template": self.template, + }, + context_instance=RequestContext(request)) + + def success(self, form, request): + """What to do when the form is valid. + + By default, just save the form. + + """ + return form.save(request) diff --git a/apps/api/handlers.py b/apps/api/handlers.py index 8f06deafc..f99dd90ab 100644 --- a/apps/api/handlers.py +++ b/apps/api/handlers.py @@ -15,6 +15,7 @@ from api.helpers import timestamp from api.models import Deleted from catalogue.forms import BookImportForm from catalogue.models import Book, Tag, BookMedia, Fragment +from picture.models import Picture from stats.utils import piwik_track @@ -93,15 +94,18 @@ class BookDetailHandler(BaseHandler): """ allowed_methods = ['GET'] - fields = ['title', 'parent'] + Book.file_types + [ + fields = ['title', 'parent'] + Book.formats + [ 'media', 'url'] + category_singular.keys() @piwik_track - def read(self, request, slug): - """ Returns details of a book, identified by a slug. """ + def read(self, request, book): + """ Returns details of a book, identified by a slug and lang. """ + kwargs = Book.split_urlid(book) + if not kwargs: + return rc.NOT_FOUND try: - return Book.objects.get(slug=slug) + return Book.objects.get(**kwargs) except Book.DoesNotExist: return rc.NOT_FOUND @@ -122,7 +126,7 @@ class AnonymousBooksHandler(AnonymousBaseHandler): @classmethod def href(cls, book): """ Returns an URI for a Book in the API. """ - return API_BASE + reverse("api_book", args=[book.slug]) + return API_BASE + reverse("api_book", args=[book.urlid()]) @classmethod def url(cls, book): @@ -202,7 +206,7 @@ def _file_getter(format): else: return '' return get_file -for format in Book.file_types: +for format in Book.formats: setattr(BooksHandler, format, _file_getter(format)) @@ -246,9 +250,8 @@ class TagsHandler(BaseHandler): except KeyError, e: return rc.NOT_FOUND - tags = Tag.objects.filter(category=category_sng) - tags = [t for t in tags if t.get_count() > 0] - if tags: + tags = Tag.objects.filter(category=category_sng).exclude(book_count=0) + if tags.exists(): return tags else: return rc.NOT_FOUND @@ -265,11 +268,18 @@ class FragmentDetailHandler(BaseHandler): fields = ['book', 'anchor', 'text', 'url', 'themes'] @piwik_track - def read(self, request, slug, anchor): + def read(self, request, book, anchor): """ Returns details of a fragment, identified by book slug and anchor. """ + kwargs = Book.split_urlid(book) + if not kwargs: + return rc.NOT_FOUND + + fragment_kwargs = {} + for field, value in kwargs.items(): + fragment_kwargs['book__' + field] = value try: - return Fragment.objects.get(book__slug=slug, anchor=anchor) + return Fragment.objects.get(anchor=anchor, **fragment_kwargs) except Fragment.DoesNotExist: return rc.NOT_FOUND @@ -306,7 +316,7 @@ class FragmentsHandler(BaseHandler): def href(cls, fragment): """ Returns URI in the API for the fragment. """ - return API_BASE + reverse("api_fragment", args=[fragment.book.slug, fragment.anchor]) + return API_BASE + reverse("api_fragment", args=[fragment.book.urlid(), fragment.anchor]) @classmethod def url(cls, fragment): @@ -354,8 +364,7 @@ class CatalogueHandler(BaseHandler): def book_dict(book, fields=None): all_fields = ['url', 'title', 'description', 'gazeta_link', 'wiki_link', - ] + Book.file_types + [ - 'mp3', 'ogg', 'daisy', + ] + Book.formats + BookMedia.formats + [ 'parent', 'parent_number', 'tags', 'license', 'license_description', 'source_name', @@ -372,7 +381,7 @@ class CatalogueHandler(BaseHandler): obj = {} for field in fields: - if field in Book.file_types: + if field in Book.formats: f = getattr(book, field+'_file') if f: obj[field] = { @@ -380,7 +389,7 @@ class CatalogueHandler(BaseHandler): 'size': f.size, } - elif field in ('mp3', 'ogg', 'daisy'): + elif field in BookMedia.formats: media = [] for m in book.media.filter(type=field): media.append({ @@ -509,7 +518,7 @@ class CatalogueHandler(BaseHandler): changed_at__gte=since, changed_at__lt=until): # only serve non-empty tags - if tag.get_count(): + if tag.book_count: tag_d = cls.tag_dict(tag, fields) updated.append(tag_d) elif tag.created_at < since: @@ -572,3 +581,21 @@ class ChangesHandler(CatalogueHandler): @piwik_track def read(self, request, since): return self.changes(request, since) + + +class PictureHandler(BaseHandler): + model = Picture + fields = ('slug', 'title') + allowed_methods = ('POST',) + + def create(self, request): + if not request.user.has_perm('catalogue.add_book'): + return rc.FORBIDDEN + + data = json.loads(request.POST.get('data')) + form = BookImportForm(data) + if form.is_valid(): + form.save() + return rc.CREATED + else: + return rc.NOT_FOUND diff --git a/apps/api/management/commands/mobileinit.py b/apps/api/management/commands/mobileinit.py index 658b1770d..2abbfb3d3 100755 --- a/apps/api/management/commands/mobileinit.py +++ b/apps/api/management/commands/mobileinit.py @@ -23,10 +23,10 @@ class Command(BaseCommand): db = init_db(last_checked) for b in Book.objects.all(): add_book(db, b) - for t in Tag.objects.exclude(category__in=('book', 'set', 'theme')): + for t in Tag.objects.exclude( + category__in=('book', 'set', 'theme')).exclude(book_count=0): # only add non-empty tags - if t.get_count(): - add_tag(db, t) + add_tag(db, t) db.commit() db.close() current(last_checked) diff --git a/apps/api/tests.py b/apps/api/tests.py index 2c2e51ce8..74417acb9 100644 --- a/apps/api/tests.py +++ b/apps/api/tests.py @@ -8,6 +8,13 @@ from django.conf import settings from api.helpers import timestamp from catalogue.models import Book, Tag +from picture.tests.utils import RequestFactory +from picture.forms import PictureImportForm +from picture.models import Picture +import picture.tests +from django.core.files.uploadedfile import SimpleUploadedFile + +from os import path class ApiTest(TestCase): @@ -135,3 +142,21 @@ class TagTests(TestCase): tag = json.loads(self.client.get('/api/authors/joe/').content) self.assertEqual(tag['name'], self.tag.name, 'Wrong tag details.') + + +class PictureTests(ApiTest): + def test_publish(self): + slug = "kandinsky-composition-viii" + xml = SimpleUploadedFile('composition8.xml', open(path.join(picture.tests.__path__[0], "files", slug + ".xml")).read()) + img = SimpleUploadedFile('kompozycja-8.png', open(path.join(picture.tests.__path__[0], "files", slug + ".png")).read()) + + import_form = PictureImportForm({}, { + 'picture_xml_file': xml, + 'picture_image_file': img + }) + + assert import_form.is_valid() + if import_form.is_valid(): + import_form.save() + + pic = Picture.objects.get(slug=slug) diff --git a/apps/api/urls.py b/apps/api/urls.py index 2d92eba46..60b20647c 100644 --- a/apps/api/urls.py +++ b/apps/api/urls.py @@ -4,7 +4,7 @@ from piston.authentication import OAuthAuthentication from piston.resource import Resource from api import handlers - +from catalogue.models import Book auth = OAuthAuthentication(realm="Wolne Lektury") @@ -22,6 +22,7 @@ tag_resource = Resource(handler=handlers.TagDetailHandler) fragment_resource = Resource(handler=handlers.FragmentDetailHandler) fragment_list_resource = Resource(handler=handlers.FragmentsHandler) +picture_resource = Resource(handler=handlers.PictureHandler, authentication=auth) urlpatterns = patterns( 'piston.authentication', @@ -46,10 +47,10 @@ urlpatterns = patterns( # objects details - url(r'^books/(?P[a-z0-9-]+)/$', book_resource, name="api_book"), + url(r'^books/(?P%s)/$' % Book.URLID_RE, book_resource, name="api_book"), url(r'^(?P[a-z0-9-]+)/(?P[a-z0-9-]+)/$', tag_resource, name="api_tag"), - url(r'^books/(?P[a-z0-9-]+)/fragments/(?P[a-z0-9-]+)/$', + url(r'^books/(?P%s)/fragments/(?P[a-z0-9-]+)/$' % Book.URLID_RE, fragment_resource, name="api_fragment"), # books by tags @@ -62,4 +63,7 @@ urlpatterns = patterns( # tags by category url(r'^(?P[a-z0-9-]+)/$', tag_list_resource), + + # picture by slug + url(r'^pictures/$', picture_resource) ) diff --git a/apps/catalogue/admin.py b/apps/catalogue/admin.py index 88c985f3c..7ef2aca6f 100644 --- a/apps/catalogue/admin.py +++ b/apps/catalogue/admin.py @@ -10,7 +10,7 @@ from catalogue.models import Tag, Book, Fragment, BookMedia class TagAdmin(admin.ModelAdmin): - list_display = ('name', 'slug', 'sort_key', 'category', 'has_description', 'main_page',) + list_display = ('name', 'slug', 'sort_key', 'category', 'has_description',) list_filter = ('category',) search_fields = ('name',) ordering = ('name',) diff --git a/apps/catalogue/forms.py b/apps/catalogue/forms.py index 92f50edcb..b7bf249f8 100644 --- a/apps/catalogue/forms.py +++ b/apps/catalogue/forms.py @@ -3,7 +3,6 @@ # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information. # from django import forms -from django.core.files.base import ContentFile from django.utils.translation import ugettext_lazy as _ from slughifi import slughifi @@ -17,6 +16,8 @@ class BookImportForm(forms.Form): book_xml = forms.CharField(required=False) def clean(self): + from django.core.files.base import ContentFile + if not self.cleaned_data['book_xml_file']: if self.cleaned_data['book_xml']: self.cleaned_data['book_xml_file'] = \ @@ -60,7 +61,7 @@ class ObjectSetsForm(forms.Form): self.fields['set_ids'] = forms.MultipleChoiceField( label=_('Shelves'), required=False, - choices=[(tag.id, "%s (%s)" % (tag.name, tag.get_count())) for tag in Tag.objects.filter(category='set', user=user)], + choices=[(tag.id, "%s (%s)" % (tag.name, tag.book_count)) for tag in Tag.objects.filter(category='set', user=user)], initial=[tag.id for tag in obj.tags.filter(category='set', user=user)], widget=forms.CheckboxSelectMultiple ) @@ -82,21 +83,62 @@ class NewSetForm(forms.Form): return new_set -FORMATS = ( - ('mp3', 'MP3'), - ('ogg', 'OGG'), - ('pdf', 'PDF'), - ('odt', 'ODT'), - ('txt', 'TXT'), - ('epub', 'EPUB'), - ('daisy', 'DAISY'), - ('mobi', 'MOBI'), -) +FORMATS = [(f, f.upper()) for f in Book.ebook_formats] class DownloadFormatsForm(forms.Form): - formats = forms.MultipleChoiceField(required=False, choices=FORMATS, widget=forms.CheckboxSelectMultiple) + formats = forms.MultipleChoiceField(required=False, choices=FORMATS, + widget=forms.CheckboxSelectMultiple) def __init__(self, *args, **kwargs): super(DownloadFormatsForm, self).__init__(*args, **kwargs) + +PDF_PAGE_SIZES = ( + ('a4paper', _('A4')), + ('a5paper', _('A5')), +) + + +PDF_LEADINGS = ( + ('', _('Normal leading')), + ('onehalfleading', _('One and a half leading')), + ('doubleleading', _('Double leading')), + ) + +PDF_FONT_SIZES = ( + ('11pt', _('Default')), + ('13pt', _('Big')) + ) + + +class CustomPDFForm(forms.Form): + nofootnotes = forms.BooleanField(required=False, label=_("Don't show footnotes")) + nothemes = forms.BooleanField(required=False, label=_("Don't disply themes")) + nowlfont = forms.BooleanField(required=False, label=_("Don't use our custom font")) + ## pagesize = forms.ChoiceField(PDF_PAGE_SIZES, required=True, label=_("Paper size")) + leading = forms.ChoiceField(PDF_LEADINGS, required=False, label=_("Leading")) + fontsize = forms.ChoiceField(PDF_FONT_SIZES, required=True, label=_("Font size")) + + @property + def customizations(self): + c = [] + if self.cleaned_data['nofootnotes']: + c.append('nofootnotes') + + if self.cleaned_data['nothemes']: + c.append('nothemes') + + if self.cleaned_data['nowlfont']: + c.append('nowlfont') + + ## c.append(self.cleaned_data['pagesize']) + c.append(self.cleaned_data['fontsize']) + + if self.cleaned_data['leading']: + c.append(self.cleaned_data['leading']) + + c.sort() + + return c + diff --git a/apps/catalogue/management/commands/importbooks.py b/apps/catalogue/management/commands/importbooks.py index 4ea0fd359..b6ddc5540 100644 --- a/apps/catalogue/management/commands/importbooks.py +++ b/apps/catalogue/management/commands/importbooks.py @@ -12,6 +12,7 @@ from django.core.management.color import color_style from django.core.files import File from catalogue.models import Book +from picture.models import Picture class Command(BaseCommand): @@ -32,10 +33,36 @@ class Command(BaseCommand): help='Don\'t build PDF file'), make_option('-w', '--wait-until', dest='wait_until', metavar='TIME', help='Wait until specified time (Y-M-D h:m:s)'), + make_option('-p', '--picture', action='store_true', dest='import_picture', default=False, + help='Import pictures'), ) help = 'Imports books from the specified directories.' args = 'directory [directory ...]' + def import_book(self, file_path, options): + verbose = options.get('verbose') + file_base, ext = os.path.splitext(file_path) + book = Book.from_xml_file(file_path, overwrite=options.get('force'), + build_epub=options.get('build_epub'), + build_txt=options.get('build_txt'), + build_pdf=options.get('build_pdf'), + build_mobi=options.get('build_mobi'), + search_index=options.get('search_index')) + fileid = book.fileid() + for ebook_format in Book.ebook_formats: + if os.path.isfile(file_base + '.' + ebook_format): + getattr(book, '%s_file' % ebook_format).save( + '%s.%s' % (fileid, ebook_format), + File(file(file_base + '.' + ebook_format))) + if verbose: + print "Importing %s.%s" % (file_base, ebook_format) + + book.save() + + def import_picture(self, file_path, options): + picture = Picture.from_xml_file(file_path, overwrite=options.get('force')) + return picture + def handle(self, *directories, **options): from django.db import transaction @@ -44,13 +71,14 @@ class Command(BaseCommand): verbose = options.get('verbose') force = options.get('force') show_traceback = options.get('traceback', False) + import_picture = options.get('import_picture') wait_until = None if options.get('wait_until'): wait_until = time.mktime(time.strptime(options.get('wait_until'), '%Y-%m-%d %H:%M:%S')) if verbose > 0: print "Will wait until %s; it's %f seconds from now" % ( - time.strftime('%Y-%m-%d %H:%M:%S', + time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(wait_until)), wait_until - time.time()) # Start transaction management. @@ -85,35 +113,14 @@ class Command(BaseCommand): # Import book files try: - book = Book.from_xml_file(file_path, overwrite=force, - build_epub=options.get('build_epub'), - build_txt=options.get('build_txt'), - build_pdf=options.get('build_pdf'), - build_mobi=options.get('build_mobi'), - search_index=options.get('search_index')) + if import_picture: + self.import_picture(file_path, options) + else: + self.import_book(file_path, options) files_imported += 1 - if os.path.isfile(file_base + '.pdf'): - book.pdf_file.save('%s.pdf' % book.slug, File(file(file_base + '.pdf'))) - if verbose: - print "Importing %s.pdf" % file_base - if os.path.isfile(file_base + '.mobi'): - book.mobi_file.save('%s.mobi' % book.slug, File(file(file_base + '.mobi'))) - if verbose: - print "Importing %s.mobi" % file_base - if os.path.isfile(file_base + '.epub'): - book.epub_file.save('%s.epub' % book.slug, File(file(file_base + '.epub'))) - if verbose: - print "Importing %s.epub" % file_base - if os.path.isfile(file_base + '.txt'): - book.txt_file.save('%s.txt' % book.slug, File(file(file_base + '.txt'))) - if verbose: - print "Importing %s.txt" % file_base - - book.save() - - except Book.AlreadyExists, msg: - print self.style.ERROR('%s: Book already imported. Skipping. To overwrite use --force.' % + except (Book.AlreadyExists, Picture.AlreadyExists): + print self.style.ERROR('%s: Book or Picture already imported. Skipping. To overwrite use --force.' % file_path) files_skipped += 1 diff --git a/apps/catalogue/management/commands/pack.py b/apps/catalogue/management/commands/pack.py index c75f092a9..280c0f6ad 100755 --- a/apps/catalogue/management/commands/pack.py +++ b/apps/catalogue/management/commands/pack.py @@ -23,7 +23,7 @@ class Command(BaseCommand): make_option('-e', '--exclude', dest='exclude', metavar='SLUG,...', help='Exclude specific books by slug') ) - help = 'Prepare data for Lesmianator.' + help = 'Prepare ZIP package with files of given type.' args = '[%s] output_path.zip' % '|'.join(ftypes) def handle(self, ftype, path, **options): @@ -33,7 +33,7 @@ class Command(BaseCommand): include = options.get('include') exclude = options.get('exclude') - if ftype in Book.file_types: + if ftype in Book.formats: field = "%s_file" % ftype else: print self.style.ERROR('Unknown file type.') diff --git a/apps/catalogue/migrations/0017_auto__add_field_book_language__del_unique_book_slug__add_unique_book_s.py b/apps/catalogue/migrations/0017_auto__add_field_book_language__del_unique_book_slug__add_unique_book_s.py new file mode 100644 index 000000000..6d1edcfa1 --- /dev/null +++ b/apps/catalogue/migrations/0017_auto__add_field_book_language__del_unique_book_slug__add_unique_book_s.py @@ -0,0 +1,144 @@ +# encoding: utf-8 +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + +class Migration(SchemaMigration): + + def forwards(self, orm): + + # Removing unique constraint on 'Book', fields ['slug'] + db.delete_unique('catalogue_book', ['slug']) + + # Adding field 'Book.language' + db.add_column('catalogue_book', 'language', self.gf('django.db.models.fields.CharField')(default='pol', max_length=3, db_index=True), keep_default=False) + + # Adding unique constraint on 'Book', fields ['slug', 'language'] + db.create_unique('catalogue_book', ['slug', 'language']) + + + def backwards(self, orm): + + # Removing unique constraint on 'Book', fields ['slug', 'language'] + db.delete_unique('catalogue_book', ['slug', 'language']) + + # Deleting field 'Book.language' + db.delete_column('catalogue_book', 'language') + + # Adding unique constraint on 'Book', fields ['slug'] + db.create_unique('catalogue_book', ['slug']) + + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'catalogue.book': { + 'Meta': {'ordering': "('sort_key',)", 'unique_together': "[['slug', 'language']]", 'object_name': 'Book'}, + 'changed_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}), + 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'epub_file': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'blank': 'True'}), + 'extra_info': ('catalogue.fields.JSONField', [], {'default': "'{}'"}), + 'gazeta_link': ('django.db.models.fields.CharField', [], {'max_length': '240', 'blank': 'True'}), + 'html_file': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'language': ('django.db.models.fields.CharField', [], {'default': "'pol'", 'max_length': '3', 'db_index': 'True'}), + 'mobi_file': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'blank': 'True'}), + 'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': "orm['catalogue.Book']"}), + 'parent_number': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'pdf_file': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'blank': 'True'}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '120', 'db_index': 'True'}), + 'sort_key': ('django.db.models.fields.CharField', [], {'max_length': '120', 'db_index': 'True'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '120'}), + 'txt_file': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'blank': 'True'}), + 'wiki_link': ('django.db.models.fields.CharField', [], {'max_length': '240', 'blank': 'True'}), + 'xml_file': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'blank': 'True'}) + }, + 'catalogue.bookmedia': { + 'Meta': {'ordering': "('type', 'name')", 'object_name': 'BookMedia'}, + 'book': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'media'", 'to': "orm['catalogue.Book']"}), + 'extra_info': ('catalogue.fields.JSONField', [], {'default': "'{}'"}), + 'file': ('catalogue.fields.OverwritingFileField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': "'100'"}), + 'source_sha1': ('django.db.models.fields.CharField', [], {'max_length': '40', 'null': 'True', 'blank': 'True'}), + 'type': ('django.db.models.fields.CharField', [], {'max_length': "'100'"}), + 'uploaded_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}) + }, + 'catalogue.filerecord': { + 'Meta': {'ordering': "('-time', '-slug', '-type')", 'object_name': 'FileRecord'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'sha1': ('django.db.models.fields.CharField', [], {'max_length': '40'}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '120', 'db_index': 'True'}), + 'time': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'type': ('django.db.models.fields.CharField', [], {'max_length': '20', 'db_index': 'True'}) + }, + 'catalogue.fragment': { + 'Meta': {'ordering': "('book', 'anchor')", 'object_name': 'Fragment'}, + 'anchor': ('django.db.models.fields.CharField', [], {'max_length': '120'}), + 'book': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'fragments'", 'to': "orm['catalogue.Book']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'short_text': ('django.db.models.fields.TextField', [], {}), + 'text': ('django.db.models.fields.TextField', [], {}) + }, + 'catalogue.tag': { + 'Meta': {'ordering': "('sort_key',)", 'unique_together': "(('slug', 'category'),)", 'object_name': 'Tag'}, + 'book_count': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), + 'category': ('django.db.models.fields.CharField', [], {'max_length': '50', 'db_index': 'True'}), + 'changed_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}), + 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'gazeta_link': ('django.db.models.fields.CharField', [], {'max_length': '240', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'main_page': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50', 'db_index': 'True'}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '120', 'db_index': 'True'}), + 'sort_key': ('django.db.models.fields.CharField', [], {'max_length': '120', 'db_index': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}), + 'wiki_link': ('django.db.models.fields.CharField', [], {'max_length': '240', 'blank': 'True'}) + }, + 'catalogue.tagrelation': { + 'Meta': {'unique_together': "(('tag', 'content_type', 'object_id'),)", 'object_name': 'TagRelation', 'db_table': "'catalogue_tag_relation'"}, + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}), + 'tag': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'items'", 'to': "orm['catalogue.Tag']"}) + }, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + } + } + + complete_apps = ['catalogue'] diff --git a/apps/catalogue/migrations/0018_auto__del_filerecord.py b/apps/catalogue/migrations/0018_auto__del_filerecord.py new file mode 100644 index 000000000..66a6542a3 --- /dev/null +++ b/apps/catalogue/migrations/0018_auto__del_filerecord.py @@ -0,0 +1,131 @@ +# encoding: utf-8 +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + +class Migration(SchemaMigration): + + def forwards(self, orm): + + # Deleting model 'FileRecord' + db.delete_table('catalogue_filerecord') + + + def backwards(self, orm): + + # Adding model 'FileRecord' + db.create_table('catalogue_filerecord', ( + ('sha1', self.gf('django.db.models.fields.CharField')(max_length=40)), + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('time', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)), + ('type', self.gf('django.db.models.fields.CharField')(max_length=20, db_index=True)), + ('slug', self.gf('django.db.models.fields.SlugField')(max_length=120, db_index=True)), + )) + db.send_create_signal('catalogue', ['FileRecord']) + + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'catalogue.book': { + 'Meta': {'ordering': "('sort_key',)", 'unique_together': "[['slug', 'language']]", 'object_name': 'Book'}, + 'changed_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}), + 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'epub_file': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'blank': 'True'}), + 'extra_info': ('catalogue.fields.JSONField', [], {'default': "'{}'"}), + 'gazeta_link': ('django.db.models.fields.CharField', [], {'max_length': '240', 'blank': 'True'}), + 'html_file': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'language': ('django.db.models.fields.CharField', [], {'default': "'pol'", 'max_length': '3', 'db_index': 'True'}), + 'mobi_file': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'blank': 'True'}), + 'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': "orm['catalogue.Book']"}), + 'parent_number': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'pdf_file': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'blank': 'True'}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '120', 'db_index': 'True'}), + 'sort_key': ('django.db.models.fields.CharField', [], {'max_length': '120', 'db_index': 'True'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '120'}), + 'txt_file': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'blank': 'True'}), + 'wiki_link': ('django.db.models.fields.CharField', [], {'max_length': '240', 'blank': 'True'}), + 'xml_file': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'blank': 'True'}) + }, + 'catalogue.bookmedia': { + 'Meta': {'ordering': "('type', 'name')", 'object_name': 'BookMedia'}, + 'book': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'media'", 'to': "orm['catalogue.Book']"}), + 'extra_info': ('catalogue.fields.JSONField', [], {'default': "'{}'"}), + 'file': ('catalogue.fields.OverwritingFileField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': "'100'"}), + 'source_sha1': ('django.db.models.fields.CharField', [], {'max_length': '40', 'null': 'True', 'blank': 'True'}), + 'type': ('django.db.models.fields.CharField', [], {'max_length': "'100'"}), + 'uploaded_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}) + }, + 'catalogue.fragment': { + 'Meta': {'ordering': "('book', 'anchor')", 'object_name': 'Fragment'}, + 'anchor': ('django.db.models.fields.CharField', [], {'max_length': '120'}), + 'book': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'fragments'", 'to': "orm['catalogue.Book']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'short_text': ('django.db.models.fields.TextField', [], {}), + 'text': ('django.db.models.fields.TextField', [], {}) + }, + 'catalogue.tag': { + 'Meta': {'ordering': "('sort_key',)", 'unique_together': "(('slug', 'category'),)", 'object_name': 'Tag'}, + 'book_count': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), + 'category': ('django.db.models.fields.CharField', [], {'max_length': '50', 'db_index': 'True'}), + 'changed_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}), + 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'gazeta_link': ('django.db.models.fields.CharField', [], {'max_length': '240', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'main_page': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50', 'db_index': 'True'}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '120', 'db_index': 'True'}), + 'sort_key': ('django.db.models.fields.CharField', [], {'max_length': '120', 'db_index': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}), + 'wiki_link': ('django.db.models.fields.CharField', [], {'max_length': '240', 'blank': 'True'}) + }, + 'catalogue.tagrelation': { + 'Meta': {'unique_together': "(('tag', 'content_type', 'object_id'),)", 'object_name': 'TagRelation', 'db_table': "'catalogue_tag_relation'"}, + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}), + 'tag': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'items'", 'to': "orm['catalogue.Tag']"}) + }, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + } + } + + complete_apps = ['catalogue'] diff --git a/apps/catalogue/migrations/0019_auto__add_field_book_cover.py b/apps/catalogue/migrations/0019_auto__add_field_book_cover.py new file mode 100644 index 000000000..259d935ba --- /dev/null +++ b/apps/catalogue/migrations/0019_auto__add_field_book_cover.py @@ -0,0 +1,125 @@ +# encoding: utf-8 +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + +class Migration(SchemaMigration): + + def forwards(self, orm): + + # Adding field 'Book.cover' + db.add_column('catalogue_book', 'cover', self.gf('django.db.models.fields.files.FileField')(max_length=100, null=True, blank=True), keep_default=False) + + + def backwards(self, orm): + + # Deleting field 'Book.cover' + db.delete_column('catalogue_book', 'cover') + + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'catalogue.book': { + 'Meta': {'ordering': "('sort_key',)", 'unique_together': "[['slug', 'language']]", 'object_name': 'Book'}, + 'changed_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}), + 'cover': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), + 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'epub_file': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'blank': 'True'}), + 'extra_info': ('catalogue.fields.JSONField', [], {'default': "'{}'"}), + 'gazeta_link': ('django.db.models.fields.CharField', [], {'max_length': '240', 'blank': 'True'}), + 'html_file': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'language': ('django.db.models.fields.CharField', [], {'default': "'pol'", 'max_length': '3', 'db_index': 'True'}), + 'mobi_file': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'blank': 'True'}), + 'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': "orm['catalogue.Book']"}), + 'parent_number': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'pdf_file': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'blank': 'True'}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '120', 'db_index': 'True'}), + 'sort_key': ('django.db.models.fields.CharField', [], {'max_length': '120', 'db_index': 'True'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '120'}), + 'txt_file': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'blank': 'True'}), + 'wiki_link': ('django.db.models.fields.CharField', [], {'max_length': '240', 'blank': 'True'}), + 'xml_file': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'blank': 'True'}) + }, + 'catalogue.bookmedia': { + 'Meta': {'ordering': "('type', 'name')", 'object_name': 'BookMedia'}, + 'book': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'media'", 'to': "orm['catalogue.Book']"}), + 'extra_info': ('catalogue.fields.JSONField', [], {'default': "'{}'"}), + 'file': ('catalogue.fields.OverwritingFileField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': "'100'"}), + 'source_sha1': ('django.db.models.fields.CharField', [], {'max_length': '40', 'null': 'True', 'blank': 'True'}), + 'type': ('django.db.models.fields.CharField', [], {'max_length': "'100'"}), + 'uploaded_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}) + }, + 'catalogue.fragment': { + 'Meta': {'ordering': "('book', 'anchor')", 'object_name': 'Fragment'}, + 'anchor': ('django.db.models.fields.CharField', [], {'max_length': '120'}), + 'book': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'fragments'", 'to': "orm['catalogue.Book']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'short_text': ('django.db.models.fields.TextField', [], {}), + 'text': ('django.db.models.fields.TextField', [], {}) + }, + 'catalogue.tag': { + 'Meta': {'ordering': "('sort_key',)", 'unique_together': "(('slug', 'category'),)", 'object_name': 'Tag'}, + 'book_count': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), + 'category': ('django.db.models.fields.CharField', [], {'max_length': '50', 'db_index': 'True'}), + 'changed_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}), + 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'gazeta_link': ('django.db.models.fields.CharField', [], {'max_length': '240', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'main_page': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50', 'db_index': 'True'}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '120', 'db_index': 'True'}), + 'sort_key': ('django.db.models.fields.CharField', [], {'max_length': '120', 'db_index': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}), + 'wiki_link': ('django.db.models.fields.CharField', [], {'max_length': '240', 'blank': 'True'}) + }, + 'catalogue.tagrelation': { + 'Meta': {'unique_together': "(('tag', 'content_type', 'object_id'),)", 'object_name': 'TagRelation', 'db_table': "'catalogue_tag_relation'"}, + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}), + 'tag': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'items'", 'to': "orm['catalogue.Tag']"}) + }, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + } + } + + complete_apps = ['catalogue'] diff --git a/apps/catalogue/migrations/0020_auto__del_field_tag_main_page.py b/apps/catalogue/migrations/0020_auto__del_field_tag_main_page.py new file mode 100644 index 000000000..e9f77946e --- /dev/null +++ b/apps/catalogue/migrations/0020_auto__del_field_tag_main_page.py @@ -0,0 +1,124 @@ +# encoding: utf-8 +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + +class Migration(SchemaMigration): + + def forwards(self, orm): + + # Deleting field 'Tag.main_page' + db.delete_column('catalogue_tag', 'main_page') + + + def backwards(self, orm): + + # Adding field 'Tag.main_page' + db.add_column('catalogue_tag', 'main_page', self.gf('django.db.models.fields.BooleanField')(default=False, db_index=True), keep_default=False) + + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'catalogue.book': { + 'Meta': {'ordering': "('sort_key',)", 'unique_together': "[['slug', 'language']]", 'object_name': 'Book'}, + 'changed_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}), + 'cover': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), + 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'epub_file': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'blank': 'True'}), + 'extra_info': ('catalogue.fields.JSONField', [], {'default': "'{}'"}), + 'gazeta_link': ('django.db.models.fields.CharField', [], {'max_length': '240', 'blank': 'True'}), + 'html_file': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'language': ('django.db.models.fields.CharField', [], {'default': "'pol'", 'max_length': '3', 'db_index': 'True'}), + 'mobi_file': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'blank': 'True'}), + 'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': "orm['catalogue.Book']"}), + 'parent_number': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'pdf_file': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'blank': 'True'}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '120', 'db_index': 'True'}), + 'sort_key': ('django.db.models.fields.CharField', [], {'max_length': '120', 'db_index': 'True'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '120'}), + 'txt_file': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'blank': 'True'}), + 'wiki_link': ('django.db.models.fields.CharField', [], {'max_length': '240', 'blank': 'True'}), + 'xml_file': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'blank': 'True'}) + }, + 'catalogue.bookmedia': { + 'Meta': {'ordering': "('type', 'name')", 'object_name': 'BookMedia'}, + 'book': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'media'", 'to': "orm['catalogue.Book']"}), + 'extra_info': ('catalogue.fields.JSONField', [], {'default': "'{}'"}), + 'file': ('catalogue.fields.OverwritingFileField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': "'100'"}), + 'source_sha1': ('django.db.models.fields.CharField', [], {'max_length': '40', 'null': 'True', 'blank': 'True'}), + 'type': ('django.db.models.fields.CharField', [], {'max_length': "'100'"}), + 'uploaded_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}) + }, + 'catalogue.fragment': { + 'Meta': {'ordering': "('book', 'anchor')", 'object_name': 'Fragment'}, + 'anchor': ('django.db.models.fields.CharField', [], {'max_length': '120'}), + 'book': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'fragments'", 'to': "orm['catalogue.Book']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'short_text': ('django.db.models.fields.TextField', [], {}), + 'text': ('django.db.models.fields.TextField', [], {}) + }, + 'catalogue.tag': { + 'Meta': {'ordering': "('sort_key',)", 'unique_together': "(('slug', 'category'),)", 'object_name': 'Tag'}, + 'book_count': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), + 'category': ('django.db.models.fields.CharField', [], {'max_length': '50', 'db_index': 'True'}), + 'changed_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}), + 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'gazeta_link': ('django.db.models.fields.CharField', [], {'max_length': '240', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50', 'db_index': 'True'}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '120', 'db_index': 'True'}), + 'sort_key': ('django.db.models.fields.CharField', [], {'max_length': '120', 'db_index': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}), + 'wiki_link': ('django.db.models.fields.CharField', [], {'max_length': '240', 'blank': 'True'}) + }, + 'catalogue.tagrelation': { + 'Meta': {'unique_together': "(('tag', 'content_type', 'object_id'),)", 'object_name': 'TagRelation', 'db_table': "'catalogue_tag_relation'"}, + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}), + 'tag': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'items'", 'to': "orm['catalogue.Tag']"}) + }, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + } + } + + complete_apps = ['catalogue'] diff --git a/apps/catalogue/migrations/0021_build_covers.py b/apps/catalogue/migrations/0021_build_covers.py new file mode 100644 index 000000000..319decb18 --- /dev/null +++ b/apps/catalogue/migrations/0021_build_covers.py @@ -0,0 +1,137 @@ +# encoding: utf-8 +import datetime +from south.db import db +from south.v2 import DataMigration +from django.db import models + +class Migration(DataMigration): + + def forwards(self, orm): + "Write your forwards methods here." + from StringIO import StringIO + from django.core.files.base import ContentFile + from librarian import ValidationError + from librarian.cover import WLCover + from librarian.dcparser import BookInfo + + for book in orm.Book.objects.filter(cover=None): + try: + book_info = BookInfo.from_file(book.xml_file.path) + except ValidationError: + pass + else: + cover = WLCover(book_info).image() + imgstr = StringIO() + cover.save(imgstr, 'png') + book.cover.save('book/png/%s.png' % book.slug, + ContentFile(imgstr.getvalue())) + + + def backwards(self, orm): + "Write your backwards methods here." + + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'catalogue.book': { + 'Meta': {'ordering': "('sort_key',)", 'unique_together': "[['slug', 'language']]", 'object_name': 'Book'}, + 'changed_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}), + 'cover': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), + 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'epub_file': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'blank': 'True'}), + 'extra_info': ('catalogue.fields.JSONField', [], {'default': "'{}'"}), + 'gazeta_link': ('django.db.models.fields.CharField', [], {'max_length': '240', 'blank': 'True'}), + 'html_file': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'language': ('django.db.models.fields.CharField', [], {'default': "'pol'", 'max_length': '3', 'db_index': 'True'}), + 'mobi_file': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'blank': 'True'}), + 'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': "orm['catalogue.Book']"}), + 'parent_number': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'pdf_file': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'blank': 'True'}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '120', 'db_index': 'True'}), + 'sort_key': ('django.db.models.fields.CharField', [], {'max_length': '120', 'db_index': 'True'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '120'}), + 'txt_file': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'blank': 'True'}), + 'wiki_link': ('django.db.models.fields.CharField', [], {'max_length': '240', 'blank': 'True'}), + 'xml_file': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'blank': 'True'}) + }, + 'catalogue.bookmedia': { + 'Meta': {'ordering': "('type', 'name')", 'object_name': 'BookMedia'}, + 'book': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'media'", 'to': "orm['catalogue.Book']"}), + 'extra_info': ('catalogue.fields.JSONField', [], {'default': "'{}'"}), + 'file': ('catalogue.fields.OverwritingFileField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': "'100'"}), + 'source_sha1': ('django.db.models.fields.CharField', [], {'max_length': '40', 'null': 'True', 'blank': 'True'}), + 'type': ('django.db.models.fields.CharField', [], {'max_length': "'100'"}), + 'uploaded_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}) + }, + 'catalogue.fragment': { + 'Meta': {'ordering': "('book', 'anchor')", 'object_name': 'Fragment'}, + 'anchor': ('django.db.models.fields.CharField', [], {'max_length': '120'}), + 'book': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'fragments'", 'to': "orm['catalogue.Book']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'short_text': ('django.db.models.fields.TextField', [], {}), + 'text': ('django.db.models.fields.TextField', [], {}) + }, + 'catalogue.tag': { + 'Meta': {'ordering': "('sort_key',)", 'unique_together': "(('slug', 'category'),)", 'object_name': 'Tag'}, + 'book_count': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), + 'category': ('django.db.models.fields.CharField', [], {'max_length': '50', 'db_index': 'True'}), + 'changed_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}), + 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'gazeta_link': ('django.db.models.fields.CharField', [], {'max_length': '240', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50', 'db_index': 'True'}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '120', 'db_index': 'True'}), + 'sort_key': ('django.db.models.fields.CharField', [], {'max_length': '120', 'db_index': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}), + 'wiki_link': ('django.db.models.fields.CharField', [], {'max_length': '240', 'blank': 'True'}) + }, + 'catalogue.tagrelation': { + 'Meta': {'unique_together': "(('tag', 'content_type', 'object_id'),)", 'object_name': 'TagRelation', 'db_table': "'catalogue_tag_relation'"}, + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}), + 'tag': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'items'", 'to': "orm['catalogue.Tag']"}) + }, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + } + } + + complete_apps = ['catalogue'] diff --git a/apps/catalogue/models.py b/apps/catalogue/models.py index 1a2e8f86f..8fb210935 100644 --- a/apps/catalogue/models.py +++ b/apps/catalogue/models.py @@ -2,16 +2,17 @@ # This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later. # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information. # -from datetime import datetime +from collections import namedtuple from django.db import models from django.db.models import permalink, Q import django.dispatch from django.core.cache import cache +from django.core.files.storage import DefaultStorage from django.utils.translation import ugettext_lazy as _ from django.contrib.auth.models import User -from django.core.files import File from django.template.loader import render_to_string +from django.utils.datastructures import SortedDict from django.utils.safestring import mark_safe from django.utils.translation import get_language from django.core.urlresolvers import reverse @@ -22,14 +23,13 @@ from django.conf import settings from newtagging.models import TagBase, tags_updated from newtagging import managers from catalogue.fields import JSONField, OverwritingFileField -from catalogue.utils import ExistingFile, ORMDocProvider, create_zip, remove_zip +from catalogue.utils import create_zip, split_tags +from catalogue.tasks import touch_tag +from shutil import copy +from glob import glob +import re +from os import path -from librarian import dcparser, html, epub, NoDublinCore -import mutagen -from mutagen import id3 -from slughifi import slughifi -from sortify import sortify -from os import unlink import search @@ -43,13 +43,6 @@ TAG_CATEGORIES = ( ('book', _('book')), ) -MEDIA_FORMATS = ( - ('odt', _('ODT file')), - ('mp3', _('MP3 file')), - ('ogg', _('OGG file')), - ('daisy', _('DAISY file')), -) - # not quite, but Django wants you to set a timeout CACHE_FOREVER = 2419200 # 28 days @@ -70,7 +63,6 @@ class Tag(TagBase): category = models.CharField(_('category'), max_length=50, blank=False, null=False, db_index=True, choices=TAG_CATEGORIES) description = models.TextField(_('description'), blank=True) - main_page = models.BooleanField(_('main page'), default=False, db_index=True, help_text=_('Show tag on main page')) user = models.ForeignKey(User, blank=True, null=True) book_count = models.IntegerField(_('book count'), blank=True, null=True) @@ -115,25 +107,22 @@ class Tag(TagBase): has_description.boolean = True def get_count(self): - """ returns global book count for book tags, fragment count for themes """ - - if self.book_count is None: - if self.category == 'book': - # never used - objects = Book.objects.none() - elif self.category == 'theme': - objects = Fragment.tagged.with_all((self,)) - else: - objects = Book.tagged.with_all((self,)).order_by() - if self.category != 'set': - # eliminate descendants - l_tags = Tag.objects.filter(slug__in=[book.book_tag_slug() for book in objects]) - descendants_keys = [book.pk for book in Book.tagged.with_any(l_tags)] - if descendants_keys: - objects = objects.exclude(pk__in=descendants_keys) - self.book_count = objects.count() - self.save() - return self.book_count + """Returns global book count for book tags, fragment count for themes.""" + + if self.category == 'book': + # never used + objects = Book.objects.none() + elif self.category == 'theme': + objects = Fragment.tagged.with_all((self,)) + else: + objects = Book.tagged.with_all((self,)).order_by() + if self.category != 'set': + # eliminate descendants + l_tags = Tag.objects.filter(slug__in=[book.book_tag_slug() for book in objects]) + descendants_keys = [book.pk for book in Book.tagged.with_any(l_tags)] + if descendants_keys: + objects = objects.exclude(pk__in=descendants_keys) + return objects.count() @staticmethod def get_tag_list(tags): @@ -177,26 +166,89 @@ class Tag(TagBase): def url_chunk(self): return '/'.join((Tag.categories_dict[self.category], self.slug)) + @staticmethod + def tags_from_info(info): + from slughifi import slughifi + from sortify import sortify + meta_tags = [] + categories = (('kinds', 'kind'), ('genres', 'genre'), ('authors', 'author'), ('epochs', 'epoch')) + for field_name, category in categories: + try: + tag_names = getattr(info, field_name) + except: + try: + tag_names = [getattr(info, category)] + except: + # For instance, Pictures do not have 'genre' field. + continue + for tag_name in tag_names: + tag_sort_key = tag_name + if category == 'author': + tag_sort_key = tag_name.last_name + tag_name = tag_name.readable() + tag, created = Tag.objects.get_or_create(slug=slughifi(tag_name), category=category) + if created: + tag.name = tag_name + tag.sort_key = sortify(tag_sort_key.lower()) + tag.save() + meta_tags.append(tag) + return meta_tags + + + +def get_dynamic_path(media, filename, ext=None, maxlen=100): + from slughifi import slughifi + + # how to put related book's slug here? + if not ext: + # BookMedia case + ext = media.formats[media.type].ext + if media is None or not media.name: + name = slughifi(filename.split(".")[0]) + else: + name = slughifi(media.name) + return 'book/%s/%s.%s' % (ext, name[:maxlen-len('book/%s/.%s' % (ext, ext))-4], ext) + # TODO: why is this hard-coded ? def book_upload_path(ext=None, maxlen=100): - def get_dynamic_path(media, filename, ext=ext): - # how to put related book's slug here? - if not ext: - if media.type == 'daisy': - ext = 'daisy.zip' - else: - ext = media.type - if not media.name: - name = slughifi(filename.split(".")[0]) - else: - name = slughifi(media.name) - return 'book/%s/%s.%s' % (ext, name[:maxlen-len('book/%s/.%s' % (ext, ext))-4], ext) - return get_dynamic_path + return lambda *args: get_dynamic_path(*args, ext=ext, maxlen=maxlen) + + +def get_customized_pdf_path(book, customizations): + """ + Returns a MEDIA_ROOT relative path for a customized pdf. The name will contain a hash of customization options. + """ + customizations.sort() + h = hash(tuple(customizations)) + + pdf_name = '%s-custom-%s' % (book.fileid(), h) + pdf_file = get_dynamic_path(None, pdf_name, ext='pdf') + + return pdf_file + + +def get_existing_customized_pdf(book): + """ + Returns a list of paths to generated customized pdf of a book + """ + pdf_glob = '%s-custom-' % (book.fileid(),) + pdf_glob = get_dynamic_path(None, pdf_glob, ext='pdf') + pdf_glob = re.sub(r"[.]([a-z0-9]+)$", "*.\\1", pdf_glob) + return glob(path.join(settings.MEDIA_ROOT, pdf_glob)) class BookMedia(models.Model): - type = models.CharField(_('type'), choices=MEDIA_FORMATS, max_length="100") + FileFormat = namedtuple("FileFormat", "name ext") + formats = SortedDict([ + ('mp3', FileFormat(name='MP3', ext='mp3')), + ('ogg', FileFormat(name='Ogg Vorbis', ext='ogg')), + ('daisy', FileFormat(name='DAISY', ext='daisy.zip')), + ]) + format_choices = [(k, _('%s file') % t.name) + for k, t in formats.items()] + + type = models.CharField(_('type'), choices=format_choices, max_length="100") name = models.CharField(_('name'), max_length="100") file = OverwritingFileField(_('file'), upload_to=book_upload_path()) uploaded_at = models.DateTimeField(_('creation date'), auto_now_add=True, editable=False) @@ -213,6 +265,9 @@ class BookMedia(models.Model): verbose_name_plural = _('book media') def save(self, *args, **kwargs): + from slughifi import slughifi + from catalogue.utils import ExistingFile, remove_zip + try: old = BookMedia.objects.get(pk=self.pk) except BookMedia.DoesNotExist, e: @@ -225,7 +280,7 @@ class BookMedia(models.Model): super(BookMedia, self).save(*args, **kwargs) # remove the zip package for book with modified media - remove_zip(self.book.slug) + remove_zip(self.book.fileid()) extra_info = self.get_extra_info_value() extra_info.update(self.read_meta()) @@ -237,6 +292,8 @@ class BookMedia(models.Model): """ Reads some metadata from the audiobook. """ + import mutagen + from mutagen import id3 artist_name = director_name = project = funded_by = '' if self.type == 'mp3': @@ -269,6 +326,8 @@ class BookMedia(models.Model): """ Reads source file SHA1 from audiobok metadata. """ + import mutagen + from mutagen import id3 if filetype == 'mp3': try: @@ -290,7 +349,9 @@ class BookMedia(models.Model): class Book(models.Model): title = models.CharField(_('title'), max_length=120) sort_key = models.CharField(_('sort key'), max_length=120, db_index=True, editable=False) - slug = models.SlugField(_('slug'), max_length=120, unique=True, db_index=True) + slug = models.SlugField(_('slug'), max_length=120, db_index=True) + language = models.CharField(_('language code'), max_length=3, db_index=True, + default=settings.CATALOGUE_DEFAULT_LANGUAGE) description = models.TextField(_('description'), blank=True) created_at = models.DateTimeField(_('creation date'), auto_now_add=True, db_index=True) changed_at = models.DateTimeField(_('creation date'), auto_now=True, db_index=True) @@ -300,19 +361,27 @@ class Book(models.Model): wiki_link = models.CharField(blank=True, max_length=240) # files generated during publication - file_types = ['epub', 'html', 'mobi', 'pdf', 'txt', 'xml'] - + cover = models.FileField(_('cover'), upload_to=book_upload_path('png'), + null=True, blank=True) + ebook_formats = ['pdf', 'epub', 'mobi', 'txt'] + formats = ebook_formats + ['html', 'xml'] + parent = models.ForeignKey('self', blank=True, null=True, related_name='children') objects = models.Manager() tagged = managers.ModelTaggedItemManager(Tag) tags = managers.TagDescriptor(Tag) html_built = django.dispatch.Signal() + published = django.dispatch.Signal() + + URLID_RE = r'[a-z0-9-]+(?:/[a-z]{3})?' + FILEID_RE = r'[a-z0-9-]+(?:_[a-z]{3})?' class AlreadyExists(Exception): pass class Meta: + unique_together = [['slug', 'language']] ordering = ('sort_key',) verbose_name = _('book') verbose_name_plural = _('books') @@ -320,7 +389,45 @@ class Book(models.Model): def __unicode__(self): return self.title + def urlid(self, sep='/'): + stem = self.slug + if self.language != settings.CATALOGUE_DEFAULT_LANGUAGE: + stem += sep + self.language + return stem + + def fileid(self): + return self.urlid('_') + + @staticmethod + def split_urlid(urlid, sep='/', default_lang=settings.CATALOGUE_DEFAULT_LANGUAGE): + """Splits a URL book id into slug and language code. + + Returns a dictionary usable i.e. for object lookup, or None. + + >>> Book.split_urlid("a-slug/pol", default_lang="eng") + {'slug': 'a-slug', 'language': 'pol'} + >>> Book.split_urlid("a-slug", default_lang="eng") + {'slug': 'a-slug', 'language': 'eng'} + >>> Book.split_urlid("a-slug_pol", "_", default_lang="eng") + {'slug': 'a-slug', 'language': 'pol'} + >>> Book.split_urlid("a-slug/eng", default_lang="eng") + + """ + parts = urlid.rsplit(sep, 1) + if len(parts) == 2: + if parts[1] == default_lang: + return None + return {'slug': parts[0], 'language': parts[1]} + else: + return {'slug': urlid, 'language': default_lang} + + @classmethod + def split_fileid(cls, fileid): + return cls.split_urlid(fileid, '_') + def save(self, force_insert=False, force_update=False, reset_short_html=True, **kwargs): + from sortify import sortify + self.sort_key = sortify(self.title) ret = super(Book, self).save(force_insert, force_update) @@ -332,14 +439,18 @@ class Book(models.Model): @permalink def get_absolute_url(self): - return ('catalogue.views.book_detail', [self.slug]) + return ('catalogue.views.book_detail', [self.urlid()]) @property def name(self): return self.title def book_tag_slug(self): - return ('l-' + self.slug)[:120] + stem = 'l-' + self.slug + if self.language != settings.CATALOGUE_DEFAULT_LANGUAGE: + return stem[:116] + ' ' + self.language + else: + return stem[:120] def book_tag(self): slug = self.book_tag_slug() @@ -351,14 +462,14 @@ class Book(models.Model): return book_tag def has_media(self, type): - if type in Book.file_types: + if type in Book.formats: return bool(getattr(self, "%s_file" % type)) else: return self.media.filter(type=type).exists() def get_media(self, type): if self.has_media(type): - if type in Book.file_types: + if type in Book.formats: return getattr(self, "%s_file" % type) else: return self.media.filter(type=type) @@ -381,6 +492,7 @@ class Book(models.Model): cache_key = "Book.short_html/%d/%s" for lang, langname in settings.LANGUAGES: cache.delete(cache_key % (self.id, lang)) + cache.delete(cache_key = "Book.mini_box/%d" % (self.id, )) # Fragment.short_html relies on book's tags, so reset it here too for fragm in self.fragments.all(): fragm.reset_short_html() @@ -395,24 +507,17 @@ class Book(models.Model): if short_html is not None: return mark_safe(short_html) else: - tags = self.tags.filter(~Q(category__in=('set', 'theme', 'book'))) - tags = [mark_safe(u'%s' % (tag.get_absolute_url(), tag.name)) for tag in tags] + tags = self.tags.filter(category__in=('author', 'kind', 'genre', 'epoch')) + tags = split_tags(tags) formats = [] # files generated during publication - if self.has_media("html"): - formats.append(u'%s' % (reverse('book_text', kwargs={'slug': self.slug}), _('Read online'))) - if self.has_media("pdf"): - formats.append(u'PDF' % self.get_media('pdf').url) - if self.has_media("mobi"): - formats.append(u'MOBI' % self.get_media('mobi').url) - if self.root_ancestor.has_media("epub"): - formats.append(u'EPUB' % self.root_ancestor.get_media('epub').url) - if self.has_media("txt"): - formats.append(u'TXT' % self.get_media('txt').url) - # other files - for m in self.media.order_by('type'): - formats.append(u'%s' % (m.file.url, m.type.upper())) + for ebook_format in self.ebook_formats: + if self.has_media(ebook_format): + formats.append(u'%s' % ( + "", #self.get_media(ebook_format).url, + ebook_format.upper() + )) formats = [mark_safe(format) for format in formats] @@ -423,17 +528,22 @@ class Book(models.Model): cache.set(cache_key, short_html, CACHE_FOREVER) return mark_safe(short_html) - @property - def root_ancestor(self): - """ returns the oldest ancestor """ + def mini_box(self): + if self.id: + cache_key = "Book.mini_box/%d" % (self.id, ) + short_html = cache.get(cache_key) + else: + short_html = None - if not hasattr(self, '_root_ancestor'): - book = self - while book.parent: - book = book.parent - self._root_ancestor = book - return self._root_ancestor + if short_html is None: + authors = self.tags.filter(category='author') + short_html = unicode(render_to_string('catalogue/book_mini_box.html', + {'book': self, 'authors': authors, 'STATIC_URL': settings.STATIC_URL})) + + if self.id: + cache.set(cache_key, short_html, CACHE_FOREVER) + return mark_safe(short_html) def has_description(self): return len(self.description) > 0 @@ -441,11 +551,6 @@ class Book(models.Model): has_description.boolean = True # ugly ugly ugly - def has_odt_file(self): - return bool(self.has_media("odt")) - has_odt_file.short_description = 'ODT' - has_odt_file.boolean = True - def has_mp3_file(self): return bool(self.has_media("mp3")) has_mp3_file.short_description = 'MP3' @@ -461,104 +566,105 @@ class Book(models.Model): has_daisy_file.short_description = 'DAISY' has_daisy_file.boolean = True - def build_pdf(self): - """ (Re)builds the pdf file. + def wldocument(self, parse_dublincore=True): + from catalogue.utils import ORMDocProvider + from librarian.parser import WLDocument + return WLDocument.from_file(self.xml_file.path, + provider=ORMDocProvider(self), + parse_dublincore=parse_dublincore) + + def build_cover(self, book_info=None): + """(Re)builds the cover image.""" + from StringIO import StringIO + from django.core.files.base import ContentFile + from librarian.cover import WLCover + + if book_info is None: + book_info = self.wldocument().book_info + + cover = WLCover(book_info).image() + imgstr = StringIO() + cover.save(imgstr, 'png') + self.cover.save(None, ContentFile(imgstr.getvalue())) + + def build_pdf(self, customizations=None, file_name=None): + """ (Re)builds the pdf file. + customizations - customizations which are passed to LaTeX class file. + file_name - save the pdf file under a different name and DO NOT save it in db. """ - from librarian import pdf - from tempfile import NamedTemporaryFile - import os + from os import unlink + from django.core.files import File + from catalogue.utils import remove_zip - try: - pdf_file = NamedTemporaryFile(delete=False) - pdf.transform(ORMDocProvider(self), - file_path=str(self.xml_file.path), - output_file=pdf_file, - ) + pdf = self.wldocument().as_pdf(customizations=customizations) - self.pdf_file.save('%s.pdf' % self.slug, File(open(pdf_file.name))) - finally: - unlink(pdf_file.name) + if file_name is None: + # we'd like to be sure not to overwrite changes happening while + # (timely) pdf generation is taking place (async celery scenario) + current_self = Book.objects.get(id=self.id) + current_self.pdf_file.save('%s.pdf' % self.fileid(), + File(open(pdf.get_filename()))) + self.pdf_file = current_self.pdf_file + + # remove cached downloadables + remove_zip(settings.ALL_PDF_ZIP) - # remove zip with all pdf files - remove_zip(settings.ALL_PDF_ZIP) + for customized_pdf in get_existing_customized_pdf(self): + unlink(customized_pdf) + else: + print "saving %s" % file_name + print "to: %s" % DefaultStorage().path(file_name) + DefaultStorage().save(file_name, File(open(pdf.get_filename()))) def build_mobi(self): """ (Re)builds the MOBI file. """ - from librarian import mobi - from tempfile import NamedTemporaryFile - import os + from django.core.files import File + from catalogue.utils import remove_zip - try: - mobi_file = NamedTemporaryFile(suffix='.mobi', delete=False) - mobi.transform(ORMDocProvider(self), verbose=1, - file_path=str(self.xml_file.path), - output_file=mobi_file.name, - ) + mobi = self.wldocument().as_mobi() - self.mobi_file.save('%s.mobi' % self.slug, File(open(mobi_file.name))) - finally: - unlink(mobi_file.name) + self.mobi_file.save('%s.mobi' % self.fileid(), File(open(mobi.get_filename()))) # remove zip with all mobi files remove_zip(settings.ALL_MOBI_ZIP) - def build_epub(self, remove_descendants=True): - """ (Re)builds the epub file. - If book has a parent, does nothing. - Unless remove_descendants is False, descendants' epubs are removed. - """ - from StringIO import StringIO - from hashlib import sha1 - from django.core.files.base import ContentFile + def build_epub(self): + """(Re)builds the epub file.""" + from django.core.files import File + from catalogue.utils import remove_zip - if self.parent: - # don't need an epub - return + epub = self.wldocument().as_epub() - epub_file = StringIO() - try: - epub.transform(ORMDocProvider(self), self.slug, output_file=epub_file) - self.epub_file.save('%s.epub' % self.slug, ContentFile(epub_file.getvalue())) - FileRecord(slug=self.slug, type='epub', sha1=sha1(epub_file.getvalue()).hexdigest()).save() - except NoDublinCore: - pass - - book_descendants = list(self.children.all()) - while len(book_descendants) > 0: - child_book = book_descendants.pop(0) - if remove_descendants and child_book.has_epub_file(): - child_book.epub_file.delete() - # save anyway, to refresh short_html - child_book.save() - book_descendants += list(child_book.children.all()) + self.epub_file.save('%s.epub' % self.fileid(), + File(open(epub.get_filename()))) # remove zip package with all epub files remove_zip(settings.ALL_EPUB_ZIP) def build_txt(self): - from StringIO import StringIO from django.core.files.base import ContentFile - from librarian import text - out = StringIO() - text.transform(open(self.xml_file.path), out) - self.txt_file.save('%s.txt' % self.slug, ContentFile(out.getvalue())) + text = self.wldocument().as_text() + self.txt_file.save('%s.txt' % self.fileid(), ContentFile(text.get_string())) def build_html(self): - from tempfile import NamedTemporaryFile from markupstring import MarkupString + from django.core.files.base import ContentFile + from slughifi import slughifi + from librarian import html meta_tags = list(self.tags.filter( category__in=('author', 'epoch', 'genre', 'kind'))) book_tag = self.book_tag() - html_file = NamedTemporaryFile() - if html.transform(self.xml_file.path, html_file, parse_dublincore=False): - self.html_file.save('%s.html' % self.slug, File(html_file)) + html_output = self.wldocument(parse_dublincore=False).as_html() + if html_output: + self.html_file.save('%s.html' % self.fileid(), + ContentFile(html_output.get_string())) # get ancestor l-tags for adding to new fragments ancestor_tags = [] @@ -608,7 +714,7 @@ class Book(models.Model): def pretty_file_name(book): return "%s/%s.%s" % ( b.get_extra_info_value()['author'], - b.slug, + b.fileid(), format_) field_name = "%s_file" % format_ @@ -622,7 +728,7 @@ class Book(models.Model): def zip_audiobooks(self): bm = BookMedia.objects.filter(book=self, type='mp3') paths = map(lambda bm: (None, bm.file.path), bm) - result = create_zip.delay(paths, self.slug) + result = create_zip.delay(paths, self.fileid()) return result.wait() def search_index(self): @@ -642,6 +748,9 @@ class Book(models.Model): @classmethod def from_xml_file(cls, xml_file, **kwargs): + from django.core.files import File + from librarian import dcparser + # use librarian to parse meta-data book_info = dcparser.parse(xml_file) @@ -658,29 +767,33 @@ class Book(models.Model): build_epub=True, build_txt=True, build_pdf=True, build_mobi=True, search_index=True): import re + from sortify import sortify # check for parts before we do anything children = [] if hasattr(book_info, 'parts'): for part_url in book_info.parts: - base, slug = part_url.rsplit('/', 1) try: - children.append(Book.objects.get(slug=slug)) + children.append(Book.objects.get( + slug=part_url.slug, language=part_url.language)) except Book.DoesNotExist, e: - raise Book.DoesNotExist(_('Book with slug = "%s" does not exist.') % slug) + raise Book.DoesNotExist(_('Book "%s/%s" does not exist.') % + (part_url.slug, part_url.language)) # Read book metadata - book_base, book_slug = book_info.url.rsplit('/', 1) + book_slug = book_info.url.slug + language = book_info.language if re.search(r'[^a-zA-Z0-9-]', book_slug): raise ValueError('Invalid characters in slug') - book, created = Book.objects.get_or_create(slug=book_slug) + book, created = Book.objects.get_or_create(slug=book_slug, language=language) if created: book_shelves = [] else: if not overwrite: - raise Book.AlreadyExists(_('Book %s already exists') % book_slug) + raise Book.AlreadyExists(_('Book %s/%s already exists') % ( + book_slug, language)) # Save shelves for this book book_shelves = list(book.tags.filter(category='set')) @@ -688,24 +801,7 @@ class Book(models.Model): book.set_extra_info_value(book_info.to_dict()) book.save() - meta_tags = [] - categories = (('kinds', 'kind'), ('genres', 'genre'), ('authors', 'author'), ('epochs', 'epoch')) - for field_name, category in categories: - try: - tag_names = getattr(book_info, field_name) - except: - tag_names = [getattr(book_info, category)] - for tag_name in tag_names: - tag_sort_key = tag_name - if category == 'author': - tag_sort_key = tag_name.last_name - tag_name = ' '.join(tag_name.first_names) + ' ' + tag_name.last_name - tag, created = Tag.objects.get_or_create(slug=slughifi(tag_name), category=category) - if created: - tag.name = tag_name - tag.sort_key = sortify(tag_sort_key.lower()) - tag.save() - meta_tags.append(tag) + meta_tags = Tag.tags_from_info(book_info) book.tags = set(meta_tags + book_shelves) @@ -726,11 +822,13 @@ class Book(models.Model): if not settings.NO_BUILD_TXT and build_txt: book.build_txt() + book.build_cover(book_info) + if not settings.NO_BUILD_EPUB and build_epub: - book.root_ancestor.build_epub() + book.build_epub() if not settings.NO_BUILD_PDF and build_pdf: - book.root_ancestor.build_pdf() + book.build_pdf() if not settings.NO_BUILD_MOBI and build_mobi: book.build_mobi() @@ -739,22 +837,27 @@ class Book(models.Model): book.search_index() book_descendants = list(book.children.all()) + descendants_tags = set() # add l-tag to descendants and their fragments - # delete unnecessary EPUB files while len(book_descendants) > 0: child_book = book_descendants.pop(0) + descendants_tags.update(child_book.tags) child_book.tags = list(child_book.tags) + [book_tag] child_book.save() for fragment in child_book.fragments.all(): fragment.tags = set(list(fragment.tags) + [book_tag]) book_descendants += list(child_book.children.all()) + for tag in descendants_tags: + touch_tag.delay(tag) + book.save() # refresh cache book.reset_tag_counter() book.reset_theme_counter() + cls.published.send(sender=book) return book def reset_tag_counter(self): @@ -848,6 +951,57 @@ class Book(models.Model): return objects + @classmethod + def book_list(cls, filter=None): + """Generates a hierarchical listing of all books. + + Books are optionally filtered with a test function. + + """ + + books_by_parent = {} + books = cls.objects.all().order_by('parent_number', 'sort_key').only( + 'title', 'parent', 'slug', 'language') + if filter: + books = books.filter(filter).distinct() + book_ids = set((book.pk for book in books)) + for book in books: + parent = book.parent_id + if parent not in book_ids: + parent = None + books_by_parent.setdefault(parent, []).append(book) + else: + for book in books: + books_by_parent.setdefault(book.parent_id, []).append(book) + + orphans = [] + books_by_author = SortedDict() + for tag in Tag.objects.filter(category='author'): + books_by_author[tag] = [] + + for book in books_by_parent.get(None,()): + authors = list(book.tags.filter(category='author')) + if authors: + for author in authors: + books_by_author[author].append(book) + else: + orphans.append(book) + + return books_by_author, orphans, books_by_parent + + _audiences_pl = { + "SP1": (1, u"szkoła podstawowa"), + "SP2": (1, u"szkoła podstawowa"), + "P": (1, u"szkoła podstawowa"), + "G": (2, u"gimnazjum"), + "L": (3, u"liceum"), + "LP": (3, u"liceum"), + } + def audiences_pl(self): + audiences = self.get_extra_info_value().get('audiences', []) + audiences = sorted(set([self._audiences_pl[a] for a in audiences])) + return [a[1] for a in audiences] + def _has_factory(ftype): has = lambda self: bool(getattr(self, "%s_file" % ftype)) @@ -858,7 +1012,7 @@ def _has_factory(ftype): # add the file fields -for t in Book.file_types: +for t in Book.formats: field_name = "%s_file" % t models.FileField(_("%s file" % t.upper()), upload_to=book_upload_path(t), @@ -883,7 +1037,7 @@ class Fragment(models.Model): verbose_name_plural = _('fragments') def get_absolute_url(self): - return '%s#m%s' % (reverse('book_text', kwargs={'slug': self.book.slug}), self.anchor) + return '%s#m%s' % (self.book.get_html_url(), self.anchor) def reset_short_html(self): if self.id is None: @@ -910,20 +1064,6 @@ class Fragment(models.Model): return mark_safe(short_html) -class FileRecord(models.Model): - slug = models.SlugField(_('slug'), max_length=120, db_index=True) - type = models.CharField(_('type'), max_length=20, db_index=True) - sha1 = models.CharField(_('sha-1 hash'), max_length=40) - time = models.DateTimeField(_('time'), auto_now_add=True) - - class Meta: - ordering = ('-time','-slug', '-type') - verbose_name = _('file record') - verbose_name_plural = _('file records') - - def __unicode__(self): - return "%s %s.%s" % (self.sha1, self.slug, self.type) - ########### # # SIGNALS @@ -934,7 +1074,8 @@ class FileRecord(models.Model): def _tags_updated_handler(sender, affected_tags, **kwargs): # reset tag global counter # we want Tag.changed_at updated for API to know the tag was touched - Tag.objects.filter(pk__in=[tag.pk for tag in affected_tags]).update(book_count=None, changed_at=datetime.now()) + for tag in affected_tags: + touch_tag.delay(tag) # if book tags changed, reset book tag counter if isinstance(sender, Book) and \ diff --git a/apps/catalogue/tasks.py b/apps/catalogue/tasks.py new file mode 100755 index 000000000..5566fe177 --- /dev/null +++ b/apps/catalogue/tasks.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +# This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later. +# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information. +# +from datetime import datetime +from celery.task import task + + +@task +def touch_tag(tag): + update_dict = { + 'book_count': tag.get_count(), + 'changed_at': datetime.now(), + } + + type(tag).objects.filter(pk=tag.pk).update(**update_dict) diff --git a/apps/catalogue/templatetags/catalogue_tags.py b/apps/catalogue/templatetags/catalogue_tags.py index e433b8e32..0ba4476ea 100644 --- a/apps/catalogue/templatetags/catalogue_tags.py +++ b/apps/catalogue/templatetags/catalogue_tags.py @@ -140,6 +140,22 @@ def book_tree(book_list, books_by_parent): else: return '' +@register.simple_tag +def book_tree_texml(book_list, books_by_parent, depth=1): + return "".join(""" + %(depth)dem%(title)s + %(audiences)s + %(audiobook)s + + %(children)s + """ % { + "depth": depth, + "title": book.title, + "audiences": ", ".join(book.audiences_pl()), + "audiobook": "audiobook" if book.has_media('mp3') else "", + "children": book_tree_texml(books_by_parent.get(book.id, ()), books_by_parent, depth + 1) + } for book in book_list) + @register.simple_tag def all_editors(extra_info): @@ -168,22 +184,6 @@ def authentication_form(): return LoginForm(prefix='login').as_ul() -@register.inclusion_tag('catalogue/search_form.html') -def search_form(): - return {"form": SearchForm()} - -@register.inclusion_tag('catalogue/breadcrumbs.html') -def breadcrumbs(tags, search_form=True): - context = {'tag_list': tags} - try: - max_tag_list = settings.MAX_TAG_LIST - except AttributeError: - max_tag_list = -1 - if search_form and (max_tag_list == -1 or len(tags) < max_tag_list): - context['search_form'] = SearchForm(tags=tags) - return context - - @register.tag def catalogue_url(parser, token): bits = token.split_contents() @@ -264,23 +264,6 @@ def tag_list(tags, choices=None): return locals() -@register.inclusion_tag('catalogue/folded_tag_list.html') -def folded_tag_list(tags, title='', choices=None): - tags = [tag for tag in tags if tag.count] - if choices is None: - choices = [] - some_tags_hidden = False - tag_count = len(tags) - - if tag_count == 1: - one_tag = tags[0] - else: - shown_tags = [tag for tag in tags if tag.main_page] - if tag_count > len(shown_tags): - some_tags_hidden = True - return locals() - - @register.inclusion_tag('catalogue/book_info.html') def book_info(book): return locals() diff --git a/apps/catalogue/test_utils.py b/apps/catalogue/test_utils.py index a5f0b4fef..58ef58acd 100644 --- a/apps/catalogue/test_utils.py +++ b/apps/catalogue/test_utils.py @@ -3,6 +3,7 @@ from django.test import TestCase import shutil import tempfile from slughifi import slughifi +from librarian import WLURI class WLTestCase(TestCase): """ @@ -23,8 +24,16 @@ class PersonStub(object): self.first_names = first_names self.last_name = last_name + def readable(self): + return " ".join(self.first_names + (self.last_name,)) + class BookInfoStub(object): + _empty_fields = ['cover_url'] + # allow single definition for multiple-value fields + _salias = { + 'authors': 'author', + } def __init__(self, **kwargs): self.__dict = kwargs @@ -35,18 +44,28 @@ class BookInfoStub(object): return object.__setattr__(self, key, value) def __getattr__(self, key): - return self.__dict[key] + try: + return self.__dict[key] + except KeyError: + if key in self._empty_fields: + return None + elif key in self._salias: + return [getattr(self, self._salias[key])] + else: + raise def to_dict(self): return dict((key, unicode(value)) for key, value in self.__dict.items()) -def info_args(title): +def info_args(title, language=None): """ generate some keywords for comfortable BookInfoCreation """ slug = unicode(slughifi(title)) + if language is None: + language = u'pol' return { 'title': unicode(title), - 'slug': slug, - 'url': u"http://wolnelektury.pl/example/%s" % slug, + 'url': WLURI.from_slug_and_lang(slug, language), 'about': u"http://wolnelektury.pl/example/URI/%s" % slug, + 'language': language, } diff --git a/apps/catalogue/tests/book_import.py b/apps/catalogue/tests/book_import.py index f65d8807a..a97c41711 100644 --- a/apps/catalogue/tests/book_import.py +++ b/apps/catalogue/tests/book_import.py @@ -4,23 +4,25 @@ from __future__ import with_statement from django.core.files.base import ContentFile, File from catalogue.test_utils import * from catalogue import models +from librarian import WLURI from nose.tools import raises import tempfile -from os import unlink,path +from os import unlink, path, makedirs class BookImportLogicTests(WLTestCase): def setUp(self): WLTestCase.setUp(self) self.book_info = BookInfoStub( - url=u"http://wolnelektury.pl/example/default-book", + url=WLURI.from_slug_and_lang(u"default-book", None), about=u"http://wolnelektury.pl/example/URI/default_book", title=u"Default Book", author=PersonStub(("Jim",), "Lazy"), kind="X-Kind", genre="X-Genre", epoch="X-Epoch", + language=u"pol", ) self.expected_tags = [ @@ -112,7 +114,7 @@ class BookImportLogicTests(WLTestCase): @raises(ValueError) def test_book_with_invalid_slug(self): """ Book with invalid characters in slug shouldn't be imported """ - self.book_info.url = "http://wolnelektury.pl/example/default_book" + self.book_info.url = WLURI.from_slug_and_lang(u"default_book", None) BOOK_TEXT = "" book = models.Book.from_text_and_meta(ContentFile(BOOK_TEXT), self.book_info) @@ -242,6 +244,38 @@ class ChildImportTests(WLTestCase): 'wrong related theme list') +class MultilingualBookImportTest(WLTestCase): + def setUp(self): + WLTestCase.setUp(self) + self.pol_info = BookInfoStub( + genre='X-Genre', + epoch='X-Epoch', + kind='X-Kind', + author=PersonStub(("Joe",), "Doe"), + **info_args("A book") + ) + + self.eng_info = BookInfoStub( + genre='X-Genre', + epoch='X-Epoch', + kind='X-Kind', + author=PersonStub(("Joe",), "Doe"), + **info_args("A book", "eng") + ) + + def test_multilingual_import(self): + BOOK_TEXT = """A""" + + book1 = models.Book.from_text_and_meta(ContentFile(BOOK_TEXT), self.pol_info) + book2 = models.Book.from_text_and_meta(ContentFile(BOOK_TEXT), self.eng_info) + + self.assertEqual( + set([b.language for b in models.Book.objects.all()]), + set(['pol', 'eng']), + 'Books imported in wrong languages.' + ) + + class BookImportGenerateTest(WLTestCase): def setUp(self): WLTestCase.setUp(self) @@ -258,3 +292,13 @@ class BookImportGenerateTest(WLTestCase): parent = models.Book.from_xml_file(input) parent.build_pdf() self.assertTrue(path.exists(parent.pdf_file.path)) + + def test_custom_pdf(self): + out = models.get_dynamic_path(None, 'test-custom', ext='pdf') + absoulute_path = path.join(settings.MEDIA_ROOT, out) + + if not path.exists(path.dirname(absoulute_path)): + makedirs(path.dirname(absoulute_path)) + + self.book.build_pdf(customizations=['nofootnotes', '13pt', 'a4paper'], file_name=out) + self.assertTrue(path.exists(absoulute_path)) diff --git a/apps/catalogue/tests/files/fraszki.xml b/apps/catalogue/tests/files/fraszki.xml index edb29abbc..90e7c1245 100755 --- a/apps/catalogue/tests/files/fraszki.xml +++ b/apps/catalogue/tests/files/fraszki.xml @@ -12,7 +12,7 @@ Fraszka -http://wolnelektury.pl/lektura/fraszki +http://wolnelektury.pl/katalog/lektura/fraszki Domena publiczna - Jan Kochanowski zm. 1584 1584 diff --git a/apps/catalogue/urls.py b/apps/catalogue/urls.py index 5d623fa92..e128c60b1 100644 --- a/apps/catalogue/urls.py +++ b/apps/catalogue/urls.py @@ -4,19 +4,20 @@ # from django.conf.urls.defaults import * from catalogue.feeds import AudiobookFeed - +from catalogue.models import Book +from picture.models import Picture urlpatterns = patterns('catalogue.views', - url(r'^$', 'main_page', name='main_page'), + url(r'^$', 'catalogue', name='catalogue'), url(r'^polki/(?P[a-zA-Z0-9-]+)/formaty/$', 'shelf_book_formats', name='shelf_book_formats'), - url(r'^polki/(?P[a-zA-Z0-9-]+)/(?P[a-zA-Z0-9-0-]+)/usun$', 'remove_from_shelf', name='remove_from_shelf'), + url(r'^polki/(?P[a-zA-Z0-9-]+)/(?P%s)/usun$' % Book.URLID_RE, 'remove_from_shelf', name='remove_from_shelf'), url(r'^polki/$', 'user_shelves', name='user_shelves'), url(r'^polki/(?P[a-zA-Z0-9-]+)/usun/$', 'delete_shelf', name='delete_shelf'), url(r'^polki/(?P[a-zA-Z0-9-]+)\.zip$', 'download_shelf', name='download_shelf'), url(r'^lektury/', 'book_list', name='book_list'), url(r'^audiobooki/$', 'audiobook_list', name='audiobook_list'), url(r'^daisy/$', 'daisy_list', name='daisy_list'), - url(r'^lektura/(?P[a-zA-Z0-9-]+)/polki/', 'book_sets', name='book_shelves'), + url(r'^lektura/(?P%s)/polki/' % Book.URLID_RE, 'book_sets', name='book_shelves'), url(r'^polki/nowa/$', 'new_set', name='new_set'), url(r'^tags/$', 'tags_starting_with', name='hint'), url(r'^jtags/$', 'json_tags_starting_with', name='jhint'), @@ -26,19 +27,23 @@ urlpatterns = patterns('catalogue.views', #url(r'^zip/pdf\.zip$', 'download_zip', {'format': 'pdf', 'slug': None}, 'download_zip_pdf'), #url(r'^zip/epub\.zip$', 'download_zip', {'format': 'epub', 'slug': None}, 'download_zip_epub'), #url(r'^zip/mobi\.zip$', 'download_zip', {'format': 'mobi', 'slug': None}, 'download_zip_mobi'), - #url(r'^zip/audiobook/(?P[a-zA-Z0-9-]+)\.zip', 'download_zip', {'format': 'audiobook'}, 'download_zip_audiobook'), - - # tools - url(r'^zegar/$', 'clock', name='clock'), + #url(r'^zip/audiobook/(?P%s)\.zip' % Book.FILEID_RE, 'download_zip', {'format': 'audiobook'}, 'download_zip_audiobook'), # Public interface. Do not change this URLs. - url(r'^lektura/(?P[a-zA-Z0-9-]+)\.html$', 'book_text', name='book_text'), - url(r'^lektura/(?P[a-zA-Z0-9-]+)/$', 'book_detail', name='book_detail'), - url(r'^lektura/(?P[a-zA-Z0-9-]+)/motyw/(?P[a-zA-Z0-9-]+)/$', + url(r'^lektura/(?P%s)\.html$' % Book.FILEID_RE, 'book_text', name='book_text'), + url(r'^lektura/(?P%s)/$' % Book.URLID_RE, 'book_detail', name='book_detail'), + url(r'^lektura/(?P%s)/motyw/(?P[a-zA-Z0-9-]+)/$' % Book.URLID_RE, 'book_fragments', name='book_fragments'), url(r'^(?P[a-zA-Z0-9-/]*)/$', 'tagged_object_list', name='tagged_object_list'), url(r'^audiobooki/(?Pmp3|ogg|daisy|all).xml$', AudiobookFeed(), name='audiobook_feed'), -) + + url(r'^custompdf/(?P%s).pdf' % Book.FILEID_RE, 'download_custom_pdf'), + +) + patterns('picture.views', + # pictures - currently pictures are coupled with catalogue, hence the url is here + url(r'^obraz/?$', 'picture_list'), + url(r'^obraz/(?P%s)/?$' % Picture.URLID_RE, 'picture_detail') + ) diff --git a/apps/catalogue/utils.py b/apps/catalogue/utils.py index 0134701a6..acbd778cd 100644 --- a/apps/catalogue/utils.py +++ b/apps/catalogue/utils.py @@ -8,7 +8,10 @@ import random import time from base64 import urlsafe_b64encode +from django.http import HttpResponse, HttpResponseRedirect, Http404, HttpResponsePermanentRedirect from django.core.files.uploadedfile import UploadedFile +from django.core.files.base import File +from django.core.files.storage import DefaultStorage from django.utils.hashcompat import sha_constructor from django.conf import settings from celery.task import task @@ -18,7 +21,9 @@ from fcntl import flock, LOCK_EX from zipfile import ZipFile from librarian import DocProvider - +from reporting.utils import read_chunks +from celery.task import task +import catalogue.models # Use the system (hardware-based) random number generator if it exists. if hasattr(random, 'SystemRandom'): @@ -61,11 +66,12 @@ class ORMDocProvider(DocProvider): def __init__(self, book): self.book = book - def by_slug(self, slug): - if slug == self.book.slug: - return self.book.xml_file + def by_slug_and_lang(self, slug, language): + if slug == self.book.slug and language == self.language: + return open(self.book.xml_file.path) else: - return type(self.book).objects.get(slug=slug).xml_file + return type(self.book).objects.get( + slug=slug, language=language).xml_file class LockFile(object): @@ -131,3 +137,30 @@ def remove_zip(zip_slug): except OSError as oe: if oe.errno != ENOENT: raise oe + + +class AttachmentHttpResponse(HttpResponse): + """Response serving a file to be downloaded. + """ + def __init__ (self, file_path, file_name, mimetype): + super(AttachmentHttpResponse, self).__init__(mimetype=mimetype) + self['Content-Disposition'] = 'attachment; filename=%s' % file_name + self.file_path = file_path + self.file_name = file_name + + with open(DefaultStorage().path(self.file_path)) as f: + for chunk in read_chunks(f): + self.write(chunk) + +@task +def async_build_pdf(book_id, customizations, file_name): + """ + A celery task to generate pdf files. + Accepts the same args as Book.build_pdf, but with book id as first parameter + instead of Book instance + """ + book = catalogue.models.Book.objects.get(id=book_id) + print "will gen %s" % DefaultStorage().path(file_name) + if not DefaultStorage().exists(file_name): + book.build_pdf(customizations=customizations, file_name=file_name) + print "done." diff --git a/apps/catalogue/views.py b/apps/catalogue/views.py index 64aada533..57a4975ac 100644 --- a/apps/catalogue/views.py +++ b/apps/catalogue/views.py @@ -2,12 +2,6 @@ # This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later. # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information. # -import tempfile -import zipfile -import tarfile -import sys -import pprint -import traceback import re import itertools from datetime import datetime @@ -23,94 +17,44 @@ from django.utils.datastructures import SortedDict from django.views.decorators.http import require_POST from django.contrib import auth from django.contrib.auth.forms import UserCreationForm, AuthenticationForm -from django.utils import simplejson -from django.utils.functional import Promise -from django.utils.encoding import force_unicode from django.utils.http import urlquote_plus from django.views.decorators import cache from django.utils import translation from django.utils.translation import ugettext as _ from django.views.generic.list_detail import object_list +from ajaxable.utils import LazyEncoder, JSONResponse from catalogue import models from catalogue import forms -from catalogue.utils import split_tags -from newtagging import views as newtagging_views +from catalogue.utils import split_tags, AttachmentHttpResponse, async_build_pdf +from catalogue.tasks import touch_tag from pdcounter import models as pdcounter_models from pdcounter import views as pdcounter_views from suggest.forms import PublishingSuggestForm -from slughifi import slughifi +from os import path staff_required = user_passes_test(lambda user: user.is_staff) -class LazyEncoder(simplejson.JSONEncoder): - def default(self, obj): - if isinstance(obj, Promise): - return force_unicode(obj) - return obj - -# shortcut for JSON reponses -class JSONResponse(HttpResponse): - def __init__(self, data={}, callback=None, **kwargs): - # get rid of mimetype - kwargs.pop('mimetype', None) - data = simplejson.dumps(data) - if callback: - data = callback + "(" + data + ");" - super(JSONResponse, self).__init__(data, mimetype="application/json", **kwargs) - - -def main_page(request): - if request.user.is_authenticated(): - shelves = models.Tag.objects.filter(category='set', user=request.user) - new_set_form = forms.NewSetForm() - - tags = models.Tag.objects.exclude(category__in=('set', 'book')) +def catalogue(request): + tags = models.Tag.objects.exclude( + category__in=('set', 'book')).exclude(book_count=0) + tags = list(tags) for tag in tags: - tag.count = tag.get_count() + tag.count = tag.book_count categories = split_tags(tags) fragment_tags = categories.get('theme', []) - form = forms.SearchForm() - return render_to_response('catalogue/main_page.html', locals(), + return render_to_response('catalogue/catalogue.html', locals(), context_instance=RequestContext(request)) def book_list(request, filter=None, template_name='catalogue/book_list.html'): """ generates a listing of all books, optionally filtered with a test function """ - form = forms.SearchForm() - - books_by_parent = {} - books = models.Book.objects.all().order_by('parent_number', 'sort_key').only('title', 'parent', 'slug') - if filter: - books = books.filter(filter).distinct() - book_ids = set((book.pk for book in books)) - for book in books: - parent = book.parent_id - if parent not in book_ids: - parent = None - books_by_parent.setdefault(parent, []).append(book) - else: - for book in books: - books_by_parent.setdefault(book.parent_id, []).append(book) - - orphans = [] - books_by_author = SortedDict() + books_by_author, orphans, books_by_parent = models.Book.book_list(filter) books_nav = SortedDict() - for tag in models.Tag.objects.filter(category='author'): - books_by_author[tag] = [] - - for book in books_by_parent.get(None,()): - authors = list(book.tags.filter(category='author')) - if authors: - for author in authors: - books_by_author[author].append(book) - else: - orphans.append(book) - for tag in books_by_author: if books_by_author[tag]: books_nav.setdefault(tag.sort_key[0], []).append(tag) @@ -237,23 +181,29 @@ def tagged_object_list(request, tags=''): ) -def book_fragments(request, book_slug, theme_slug): - book = get_object_or_404(models.Book, slug=book_slug) - book_tag = get_object_or_404(models.Tag, slug='l-' + book_slug, category='book') +def book_fragments(request, book, theme_slug): + kwargs = models.Book.split_urlid(book) + if kwargs is None: + raise Http404 + book = get_object_or_404(models.Book, **kwargs) + + book_tag = book.book_tag() theme = get_object_or_404(models.Tag, slug=theme_slug, category='theme') fragments = models.Fragment.tagged.with_all([book_tag, theme]) - form = forms.SearchForm() return render_to_response('catalogue/book_fragments.html', locals(), context_instance=RequestContext(request)) -def book_detail(request, slug): +def book_detail(request, book): + kwargs = models.Book.split_urlid(book) + if kwargs is None: + raise Http404 try: - book = models.Book.objects.get(slug=slug) + book = models.Book.objects.get(**kwargs) except models.Book.DoesNotExist: - return pdcounter_views.book_stub_detail(request, slug) - + return pdcounter_views.book_stub_detail(request, kwargs['slug']) + book_tag = book.book_tag() tags = list(book.tags.filter(~Q(category='set'))) categories = split_tags(tags) @@ -286,13 +236,17 @@ def book_detail(request, slug): projects.add((project, meta.get('funded_by', ''))) projects = sorted(projects) - form = forms.SearchForm() + custom_pdf_form = forms.CustomPDFForm() return render_to_response('catalogue/book_detail.html', locals(), context_instance=RequestContext(request)) -def book_text(request, slug): - book = get_object_or_404(models.Book, slug=slug) +def book_text(request, book): + kwargs = models.Book.split_fileid(book) + if kwargs is None: + raise Http404 + book = get_object_or_404(models.Book, **kwargs) + if not book.has_html_file(): raise Http404 book_themes = {} @@ -424,7 +378,7 @@ def books_starting_with(prefix): def find_best_matches(query, user=None): - """ Finds a Book, Tag, BookStub or Author best matching a query. + """ Finds a models.Book, Tag, models.BookStub or Author best matching a query. Returns a with: - zero elements when nothing is found, @@ -526,11 +480,15 @@ def user_shelves(request): context_instance=RequestContext(request)) @cache.never_cache -def book_sets(request, slug): +def book_sets(request, book): if not request.user.is_authenticated(): return HttpResponse(_('

To maintain your shelves you need to be logged in.

')) - book = get_object_or_404(models.Book, slug=slug) + kwargs = models.Book.split_urlid(book) + if kwargs is None: + raise Http404 + book = get_object_or_404(models.Book, **kwargs) + user_sets = models.Tag.objects.filter(category='set', user=request.user) book_sets = book.tags.filter(category='set', user=request.user) @@ -541,12 +499,10 @@ def book_sets(request, slug): new_shelves = [models.Tag.objects.get(pk=id) for id in form.cleaned_data['set_ids']] for shelf in [shelf for shelf in old_shelves if shelf not in new_shelves]: - shelf.book_count = None - shelf.save() + touch_tag(shelf) for shelf in [shelf for shelf in new_shelves if shelf not in old_shelves]: - shelf.book_count = None - shelf.save() + touch_tag(shelf) book.tags = new_shelves + list(book.tags.filter(~Q(category='set') | ~Q(user=request.user))) if request.is_ajax(): @@ -565,14 +521,16 @@ def book_sets(request, slug): @require_POST @cache.never_cache def remove_from_shelf(request, shelf, book): - book = get_object_or_404(models.Book, slug=book) + kwargs = models.Book.split_urlid(book) + if kwargs is None: + raise Http404 + book = get_object_or_404(models.Book, **kwargs) + shelf = get_object_or_404(models.Tag, slug=shelf, category='set', user=request.user) if shelf in book.tags: models.Tag.objects.remove_tag(book, shelf) - - shelf.book_count = None - shelf.save() + touch_tag(shelf) return HttpResponse(_('Book was successfully removed from the shelf')) else: @@ -599,6 +557,10 @@ def download_shelf(request, slug): without loading the whole file into memory. A similar approach can be used for large dynamic PDF files. """ + from slughifi import slughifi + import tempfile + import zipfile + shelf = get_object_or_404(models.Tag, slug=slug, category='set') formats = [] @@ -606,31 +568,18 @@ def download_shelf(request, slug): if form.is_valid(): formats = form.cleaned_data['formats'] if len(formats) == 0: - formats = ['pdf', 'epub', 'mobi', 'odt', 'txt'] + formats = models.Book.ebook_formats # Create a ZIP archive temp = tempfile.TemporaryFile() archive = zipfile.ZipFile(temp, 'w') - already = set() for book in collect_books(models.Book.tagged.with_all(shelf)): - if 'pdf' in formats and book.pdf_file: - filename = book.pdf_file.path - archive.write(filename, str('%s.pdf' % book.slug)) - if 'mobi' in formats and book.mobi_file: - filename = book.mobi_file.path - archive.write(filename, str('%s.mobi' % book.slug)) - if book.root_ancestor not in already and 'epub' in formats and book.root_ancestor.epub_file: - filename = book.root_ancestor.epub_file.path - archive.write(filename, str('%s.epub' % book.root_ancestor.slug)) - already.add(book.root_ancestor) - if 'odt' in formats and book.has_media("odt"): - for file in book.get_media("odt"): - filename = file.file.path - archive.write(filename, str('%s.odt' % slughifi(file.name))) - if 'txt' in formats and book.txt_file: - filename = book.txt_file.path - archive.write(filename, str('%s.txt' % book.slug)) + fileid = book.fileid() + for ebook_format in models.Book.ebook_formats: + if ebook_format in formats and book.has_media(ebook_format): + filename = book.get_media(ebook_format).path + archive.write(filename, str('%s.%s' % (fileid, ebook_format))) archive.close() response = HttpResponse(content_type='application/zip', mimetype='application/x-zip-compressed') @@ -649,20 +598,14 @@ def shelf_book_formats(request, shelf): """ shelf = get_object_or_404(models.Tag, slug=shelf, category='set') - formats = {'pdf': False, 'epub': False, 'mobi': False, 'odt': False, 'txt': False} + formats = {} + for ebook_format in models.Book.ebook_formats: + formats[ebook_format] = False for book in collect_books(models.Book.tagged.with_all(shelf)): - if book.pdf_file: - formats['pdf'] = True - if book.root_ancestor.epub_file: - formats['epub'] = True - if book.mobi_file: - formats['mobi'] = True - if book.txt_file: - formats['txt'] = True - for format in ('odt',): - if book.has_media(format): - formats[format] = True + for ebook_format in models.Book.ebook_formats: + if book.has_media(ebook_format): + formats[ebook_format] = True return HttpResponse(LazyEncoder().encode(formats)) @@ -696,45 +639,6 @@ def delete_shelf(request, slug): return HttpResponseRedirect('/') -# ================== -# = Authentication = -# ================== -@require_POST -@cache.never_cache -def login(request): - form = AuthenticationForm(data=request.POST, prefix='login') - if form.is_valid(): - auth.login(request, form.get_user()) - response_data = {'success': True, 'errors': {}} - else: - response_data = {'success': False, 'errors': form.errors} - return HttpResponse(LazyEncoder(ensure_ascii=False).encode(response_data)) - - -@require_POST -@cache.never_cache -def register(request): - registration_form = UserCreationForm(request.POST, prefix='registration') - if registration_form.is_valid(): - user = registration_form.save() - user = auth.authenticate( - username=registration_form.cleaned_data['username'], - password=registration_form.cleaned_data['password1'] - ) - auth.login(request, user) - response_data = {'success': True, 'errors': {}} - else: - response_data = {'success': False, 'errors': registration_form.errors} - return HttpResponse(LazyEncoder(ensure_ascii=False).encode(response_data)) - - -@cache.never_cache -def logout_then_redirect(request): - auth.logout(request) - return HttpResponseRedirect(urlquote_plus(request.GET.get('next', '/'), safe='/?=')) - - - # ========= # = Admin = # ========= @@ -747,6 +651,9 @@ def import_book(request): try: book_import_form.save() except: + import sys + import pprint + import traceback info = sys.exc_info() exception = pprint.pformat(info[1]) tb = '\n'.join(traceback.format_tb(info[2])) @@ -756,14 +663,6 @@ def import_book(request): return HttpResponse(_("Error importing file: %r") % book_import_form.errors) - -def clock(request): - """ Provides server time for jquery.countdown, - in a format suitable for Date.parse() - """ - return HttpResponse(datetime.now().strftime('%Y/%m/%d %H:%M:%S')) - - # info views for API def book_info(request, id, lang='pl'): @@ -779,13 +678,37 @@ def tag_info(request, id): return HttpResponse(tag.description) -def download_zip(request, format, slug): +def download_zip(request, format, book=None): + kwargs = models.Book.split_fileid(book) + url = None - if format in ('pdf', 'epub', 'mobi'): + if format in models.Book.ebook_formats: url = models.Book.zip_format(format) - elif format == 'audiobook' and slug is not None: - book = models.Book.objects.get(slug=slug) + elif format == 'audiobook' and kwargs is not None: + book = get_object_or_404(models.Book, **kwargs) url = book.zip_audiobooks() else: raise Http404('No format specified for zip package') return HttpResponseRedirect(urlquote_plus(settings.MEDIA_URL + url, safe='/?=')) + + +def download_custom_pdf(request, book_fileid): + kwargs = models.Book.split_fileid(book_fileid) + if kwargs is None: + raise Http404 + book = get_object_or_404(models.Book, **kwargs) + + if request.method == 'GET': + form = forms.CustomPDFForm(request.GET) + if form.is_valid(): + cust = form.customizations + pdf_file = models.get_customized_pdf_path(book, cust) + + if not path.exists(pdf_file): + result = async_build_pdf.delay(book.id, cust, pdf_file) + result.wait() + return AttachmentHttpResponse(file_name=("%s.pdf" % book_fileid), file_path=pdf_file, mimetype="application/pdf") + else: + raise Http404(_('Incorrect customization options for PDF')) + else: + raise Http404(_('Bad method')) diff --git a/apps/dictionary/templates/dictionary/note_list.html b/apps/dictionary/templates/dictionary/note_list.html index e0b10f3c2..cd88c17bc 100755 --- a/apps/dictionary/templates/dictionary/note_list.html +++ b/apps/dictionary/templates/dictionary/note_list.html @@ -1,16 +1,14 @@ {% extends "base.html" %} {% load i18n pagination_tags %} -{% load catalogue_tags %} {% block bodyid %}footnotes{% endblock %} -{% block title %}{% trans "Footnotes on WolneLektury.pl" %}{% endblock %} +{% block titleextra %}{% trans "Footnotes" %}{% endblock %} {% block body %}

{% trans "Footnotes" %}

- {% search_form %}

@@ -44,7 +42,7 @@

{% endfor %} diff --git a/apps/dictionary/views.py b/apps/dictionary/views.py index e12006366..7b9cd5313 100755 --- a/apps/dictionary/views.py +++ b/apps/dictionary/views.py @@ -7,7 +7,6 @@ from catalogue.forms import SearchForm from dictionary.models import Note def letter_notes(request, letter=None): - form = SearchForm() letters = ["0-9"] + [chr(a) for a in range(ord('a'), ord('z')+1)] objects = Note.objects.all() if letter == "0-9": diff --git a/apps/infopages/admin.py b/apps/infopages/admin.py index 66f2996c5..e5bc93cc6 100644 --- a/apps/infopages/admin.py +++ b/apps/infopages/admin.py @@ -4,6 +4,6 @@ from modeltranslation.admin import TranslationAdmin from infopages.models import InfoPage class InfoPageAdmin(TranslationAdmin): - list_display = ('title',) + list_display = ('title', 'slug', 'main_page') admin.site.register(InfoPage, InfoPageAdmin) \ No newline at end of file diff --git a/apps/infopages/migrations/0002_auto__del_field_infopage_page_title__del_field_infopage_page_title_en_.py b/apps/infopages/migrations/0002_auto__del_field_infopage_page_title__del_field_infopage_page_title_en_.py new file mode 100644 index 000000000..6ecd60b8f --- /dev/null +++ b/apps/infopages/migrations/0002_auto__del_field_infopage_page_title__del_field_infopage_page_title_en_.py @@ -0,0 +1,111 @@ +# encoding: utf-8 +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + +class Migration(SchemaMigration): + + def forwards(self, orm): + + # Deleting field 'InfoPage.page_title' + db.delete_column('infopages_infopage', 'page_title') + + # Deleting field 'InfoPage.page_title_en' + db.delete_column('infopages_infopage', 'page_title_en') + + # Deleting field 'InfoPage.page_title_es' + db.delete_column('infopages_infopage', 'page_title_es') + + # Deleting field 'InfoPage.page_title_fr' + db.delete_column('infopages_infopage', 'page_title_fr') + + # Deleting field 'InfoPage.page_title_uk' + db.delete_column('infopages_infopage', 'page_title_uk') + + # Deleting field 'InfoPage.page_title_de' + db.delete_column('infopages_infopage', 'page_title_de') + + # Deleting field 'InfoPage.page_title_lt' + db.delete_column('infopages_infopage', 'page_title_lt') + + # Deleting field 'InfoPage.page_title_pl' + db.delete_column('infopages_infopage', 'page_title_pl') + + # Deleting field 'InfoPage.page_title_ru' + db.delete_column('infopages_infopage', 'page_title_ru') + + # Adding field 'InfoPage.main_page' + db.add_column('infopages_infopage', 'main_page', self.gf('django.db.models.fields.IntegerField')(null=True, blank=True), keep_default=False) + + + def backwards(self, orm): + + # Adding field 'InfoPage.page_title' + db.add_column('infopages_infopage', 'page_title', self.gf('django.db.models.fields.CharField')(default='', max_length=120, blank=True), keep_default=False) + + # Adding field 'InfoPage.page_title_en' + db.add_column('infopages_infopage', 'page_title_en', self.gf('django.db.models.fields.CharField')(max_length=120, null=True, blank=True), keep_default=False) + + # Adding field 'InfoPage.page_title_es' + db.add_column('infopages_infopage', 'page_title_es', self.gf('django.db.models.fields.CharField')(max_length=120, null=True, blank=True), keep_default=False) + + # Adding field 'InfoPage.page_title_fr' + db.add_column('infopages_infopage', 'page_title_fr', self.gf('django.db.models.fields.CharField')(max_length=120, null=True, blank=True), keep_default=False) + + # Adding field 'InfoPage.page_title_uk' + db.add_column('infopages_infopage', 'page_title_uk', self.gf('django.db.models.fields.CharField')(max_length=120, null=True, blank=True), keep_default=False) + + # Adding field 'InfoPage.page_title_de' + db.add_column('infopages_infopage', 'page_title_de', self.gf('django.db.models.fields.CharField')(max_length=120, null=True, blank=True), keep_default=False) + + # Adding field 'InfoPage.page_title_lt' + db.add_column('infopages_infopage', 'page_title_lt', self.gf('django.db.models.fields.CharField')(max_length=120, null=True, blank=True), keep_default=False) + + # Adding field 'InfoPage.page_title_pl' + db.add_column('infopages_infopage', 'page_title_pl', self.gf('django.db.models.fields.CharField')(max_length=120, null=True, blank=True), keep_default=False) + + # Adding field 'InfoPage.page_title_ru' + db.add_column('infopages_infopage', 'page_title_ru', self.gf('django.db.models.fields.CharField')(max_length=120, null=True, blank=True), keep_default=False) + + # Deleting field 'InfoPage.main_page' + db.delete_column('infopages_infopage', 'main_page') + + + models = { + 'infopages.infopage': { + 'Meta': {'ordering': "('main_page', 'slug')", 'object_name': 'InfoPage'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'left_column': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'left_column_de': ('django.db.models.fields.TextField', [], {'null': True, 'blank': True}), + 'left_column_en': ('django.db.models.fields.TextField', [], {'null': True, 'blank': True}), + 'left_column_es': ('django.db.models.fields.TextField', [], {'null': True, 'blank': True}), + 'left_column_fr': ('django.db.models.fields.TextField', [], {'null': True, 'blank': True}), + 'left_column_lt': ('django.db.models.fields.TextField', [], {'null': True, 'blank': True}), + 'left_column_pl': ('django.db.models.fields.TextField', [], {'null': True, 'blank': True}), + 'left_column_ru': ('django.db.models.fields.TextField', [], {'null': True, 'blank': True}), + 'left_column_uk': ('django.db.models.fields.TextField', [], {'null': True, 'blank': True}), + 'main_page': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), + 'right_column': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'right_column_de': ('django.db.models.fields.TextField', [], {'null': True, 'blank': True}), + 'right_column_en': ('django.db.models.fields.TextField', [], {'null': True, 'blank': True}), + 'right_column_es': ('django.db.models.fields.TextField', [], {'null': True, 'blank': True}), + 'right_column_fr': ('django.db.models.fields.TextField', [], {'null': True, 'blank': True}), + 'right_column_lt': ('django.db.models.fields.TextField', [], {'null': True, 'blank': True}), + 'right_column_pl': ('django.db.models.fields.TextField', [], {'null': True, 'blank': True}), + 'right_column_ru': ('django.db.models.fields.TextField', [], {'null': True, 'blank': True}), + 'right_column_uk': ('django.db.models.fields.TextField', [], {'null': True, 'blank': True}), + 'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '120', 'db_index': 'True'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '120', 'blank': 'True'}), + 'title_de': ('django.db.models.fields.CharField', [], {'max_length': '120', 'null': True, 'blank': True}), + 'title_en': ('django.db.models.fields.CharField', [], {'max_length': '120', 'null': True, 'blank': True}), + 'title_es': ('django.db.models.fields.CharField', [], {'max_length': '120', 'null': True, 'blank': True}), + 'title_fr': ('django.db.models.fields.CharField', [], {'max_length': '120', 'null': True, 'blank': True}), + 'title_lt': ('django.db.models.fields.CharField', [], {'max_length': '120', 'null': True, 'blank': True}), + 'title_pl': ('django.db.models.fields.CharField', [], {'max_length': '120', 'null': True, 'blank': True}), + 'title_ru': ('django.db.models.fields.CharField', [], {'max_length': '120', 'null': True, 'blank': True}), + 'title_uk': ('django.db.models.fields.CharField', [], {'max_length': '120', 'null': True, 'blank': True}) + } + } + + complete_apps = ['infopages'] diff --git a/apps/infopages/models.py b/apps/infopages/models.py index 9fe7b3287..cf9e9bf66 100644 --- a/apps/infopages/models.py +++ b/apps/infopages/models.py @@ -6,21 +6,22 @@ from django.db import models from django.utils.translation import ugettext_lazy as _ class InfoPage(models.Model): - """ - An InfoPage is used to display a two-column flatpage - """ + """An InfoPage is used to display a two-column flatpage.""" - page_title = models.CharField(_('page title'), max_length=120, blank=True) + main_page = models.IntegerField(_('main page priority'), null=True, blank=True) slug = models.SlugField(_('slug'), max_length=120, unique=True, db_index=True) title = models.CharField(_('title'), max_length=120, blank=True) left_column = models.TextField(_('left column'), blank=True) right_column = models.TextField(_('right column'), blank=True) class Meta: - ordering = ('slug',) + ordering = ('main_page', 'slug',) verbose_name = _('info page') verbose_name_plural = _('info pages') def __unicode__(self): return self.title + @models.permalink + def get_absolute_url(self): + return ('infopage', [self.slug]) diff --git a/apps/infopages/templates/infopages/infopage.html b/apps/infopages/templates/infopages/infopage.html new file mode 100755 index 000000000..dc9efe1aa --- /dev/null +++ b/apps/infopages/templates/infopages/infopage.html @@ -0,0 +1,20 @@ +{% extends "base.html" %} +{% load i18n %} +{% load chunks %} + +{% block titleextra %}{{ page.title }}{% endblock %} + +{% block metadescription %}{{ left_column|striptags|truncatewords:10 }}{% endblock %} + +{% block body %} +

{{ page.title }}

+ + {% autoescape off %} +
+ {{ left_column }} +
+
+ {{ right_column }} +
+ {% endautoescape %} +{% endblock %} diff --git a/apps/infopages/templates/infopages/on_main.html b/apps/infopages/templates/infopages/on_main.html new file mode 100755 index 000000000..dc0103c61 --- /dev/null +++ b/apps/infopages/templates/infopages/on_main.html @@ -0,0 +1,5 @@ + diff --git a/apps/infopages/templatetags/__init__.py b/apps/infopages/templatetags/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/apps/infopages/templatetags/infopages_tags.py b/apps/infopages/templatetags/infopages_tags.py new file mode 100755 index 000000000..d7c93ca8f --- /dev/null +++ b/apps/infopages/templatetags/infopages_tags.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- +# This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later. +# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information. +# +from django import template +from infopages.models import InfoPage + +register = template.Library() + + +@register.inclusion_tag('infopages/on_main.html') +def infopages_on_main(): + objects = InfoPage.objects.exclude(main_page=None) + return {"objects": objects} diff --git a/apps/infopages/urls.py b/apps/infopages/urls.py new file mode 100755 index 000000000..081e0ef39 --- /dev/null +++ b/apps/infopages/urls.py @@ -0,0 +1,11 @@ +# -*- coding: utf-8 -*- +# This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later. +# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information. +# +from django.conf.urls.defaults import * + + +urlpatterns = patterns('infopages.views', + url(r'^(?P[a-zA-Z0-9_-]+)/$', 'infopage', name='infopage'), +) + diff --git a/apps/infopages/views.py b/apps/infopages/views.py index 07a416bbf..d457653d1 100644 --- a/apps/infopages/views.py +++ b/apps/infopages/views.py @@ -2,15 +2,19 @@ # This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later. # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information. # -from django.shortcuts import render_to_response -from django.template import RequestContext +from django.shortcuts import render_to_response, get_object_or_404 +from django.template import RequestContext, Template -from catalogue.forms import SearchForm from infopages.models import InfoPage + def infopage(request, slug): - form = SearchForm() - object = InfoPage.objects.get(slug=slug) + page = InfoPage.objects.get(slug=slug) + + page = get_object_or_404(InfoPage, slug=slug) + rc = RequestContext(request) + left_column = Template(page.left_column).render(rc) + right_column = Template(page.right_column).render(rc) - return render_to_response('info/base.html', locals(), - context_instance=RequestContext(request)) \ No newline at end of file + return render_to_response('infopages/infopage.html', locals(), + context_instance=RequestContext(request)) diff --git a/apps/lesmianator/urls.py b/apps/lesmianator/urls.py index e7bbb48d4..eeba6103f 100644 --- a/apps/lesmianator/urls.py +++ b/apps/lesmianator/urls.py @@ -3,11 +3,12 @@ # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information. # from django.conf.urls.defaults import * +from catalogue.models import Book urlpatterns = patterns('lesmianator.views', url(r'^$', 'main_page', name='lesmianator'), url(r'^wiersz/$', 'new_poem', name='new_poem'), - url(r'^lektura/(?P[a-zA-Z0-9-]+)/$', 'poem_from_book', name='poem_from_book'), + url(r'^lektura/(?P%s)/$' % Book.URLID_RE, 'poem_from_book', name='poem_from_book'), url(r'^polka/(?P[a-zA-Z0-9-]+)/$', 'poem_from_set', name='poem_from_set'), url(r'^wiersz/(?P[a-zA-Z0-9-]+)/$', 'get_poem', name='get_poem'), ) diff --git a/apps/lesmianator/views.py b/apps/lesmianator/views.py index 56acb5763..28cb32a87 100644 --- a/apps/lesmianator/views.py +++ b/apps/lesmianator/views.py @@ -1,5 +1,6 @@ # Create your views here. +from django.http import Http404 from django.shortcuts import render_to_response, get_object_or_404 from django.template import RequestContext from django.contrib.auth.decorators import login_required @@ -7,17 +8,15 @@ from django.views.decorators import cache from catalogue.utils import get_random_hash from catalogue.models import Book, Tag -from catalogue import forms from lesmianator.models import Poem, Continuations def main_page(request): last = Poem.objects.all().order_by('-created_at')[:10] - form = forms.SearchForm() shelves = Tag.objects.filter(user__username='lesmianator') return render_to_response('lesmianator/lesmianator.html', - {"last": last, "form": form, "shelves": shelves}, + {"last": last, "shelves": shelves}, context_instance=RequestContext(request)) @@ -34,8 +33,11 @@ def new_poem(request): @cache.never_cache -def poem_from_book(request, slug): - book = get_object_or_404(Book, slug=slug) +def poem_from_book(request, book): + kwargs = Book.split_urlid(book) + if kwargs is None: + raise Http404 + book = get_object_or_404(Book, **kwargs) user = request.user if request.user.is_authenticated() else None text = Poem.write(Continuations.get(book)) p = Poem(slug=get_random_hash(text), text=text, created_by=user) diff --git a/apps/lessons/views.py b/apps/lessons/views.py index 242526d86..9314d1cac 100644 --- a/apps/lessons/views.py +++ b/apps/lessons/views.py @@ -3,7 +3,6 @@ # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information. # from django.views.generic.list_detail import object_detail -from catalogue import forms from lessons import models @@ -17,6 +16,4 @@ def document_detail(request, slug): slug_field='slug', queryset=models.Document.objects.all(), template_name=template_name, - extra_context={ - 'form': forms.SearchForm(), - }) + ) diff --git a/apps/opds/views.py b/apps/opds/views.py index 44baf5b2a..c907fe198 100644 --- a/apps/opds/views.py +++ b/apps/opds/views.py @@ -234,7 +234,7 @@ class ByCategoryFeed(Feed): return feed['title'] def items(self, feed): - return (tag for tag in Tag.objects.filter(category=feed['category']) if tag.get_count() > 0) + return Tag.objects.filter(category=feed['category']).exclude(book_count=0) def item_title(self, item): return item.name @@ -285,7 +285,7 @@ class UserFeed(Feed): return u"Półki użytkownika %s" % user.username def items(self, user): - return (tag for tag in Tag.objects.filter(category='set', user=user) if tag.get_count() > 0) + return Tag.objects.filter(category='set', user=user).exclude(book_count=0) def item_title(self, item): return item.name diff --git a/apps/pdcounter/urls.py b/apps/pdcounter/urls.py deleted file mode 100644 index 232513754..000000000 --- a/apps/pdcounter/urls.py +++ /dev/null @@ -1,36 +0,0 @@ -# -*- coding: utf-8 -*- -# This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later. -# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information. -# -from django.conf.urls.defaults import * - - -urlpatterns = patterns('catalogue.views', - url(r'^$', 'main_page', name='main_page'), - url(r'^polki/(?P[a-zA-Z0-9-]+)/formaty/$', 'shelf_book_formats', name='shelf_book_formats'), - url(r'^polki/(?P[a-zA-Z0-9-]+)/(?P[a-zA-Z0-9-0-]+)/usun$', 'remove_from_shelf', name='remove_from_shelf'), - url(r'^polki/$', 'user_shelves', name='user_shelves'), - url(r'^polki/(?P[a-zA-Z0-9-]+)/usun/$', 'delete_shelf', name='delete_shelf'), - url(r'^polki/(?P[a-zA-Z0-9-]+)\.zip$', 'download_shelf', name='download_shelf'), - url(r'^lektury/', 'book_list', name='book_list'), - url(r'^audiobooki/', 'audiobook_list', name='audiobook_list'), - url(r'^daisy/', 'daisy_list', name='daisy_list'), - url(r'^lektura/(?P[a-zA-Z0-9-]+)/polki/', 'book_sets', name='book_shelves'), - url(r'^polki/nowa/$', 'new_set', name='new_set'), - url(r'^tags/$', 'tags_starting_with', name='hint'), - url(r'^jtags/$', 'json_tags_starting_with', name='jhint'), - url(r'^szukaj/$', 'search', name='search'), - - # tools - url(r'^zegar/$', 'clock', name='clock'), - url(r'^xmls.zip$', 'xmls', name='xmls'), - url(r'^epubs.tar$', 'epubs', name='epubs'), - - # Public interface. Do not change this URLs. - url(r'^lektura/(?P[a-zA-Z0-9-]+)\.html$', 'book_text', name='book_text'), - url(r'^lektura/(?P[a-zA-Z0-9-]+)/$', 'book_detail', name='book_detail'), - url(r'^lektura/(?P[a-zA-Z0-9-]+)/motyw/(?P[a-zA-Z0-9-]+)/$', - 'book_fragments', name='book_fragments'), - url(r'^(?P[a-zA-Z0-9-/]*)/$', 'tagged_object_list', name='tagged_object_list'), -) - diff --git a/apps/pdcounter/views.py b/apps/pdcounter/views.py index efcfe95ee..b07ee11e9 100644 --- a/apps/pdcounter/views.py +++ b/apps/pdcounter/views.py @@ -7,16 +7,14 @@ from django.template import RequestContext from django.shortcuts import render_to_response, get_object_or_404 from pdcounter import models -from catalogue import forms from suggest.forms import PublishingSuggestForm def book_stub_detail(request, slug): book = get_object_or_404(models.BookStub, slug=slug) pd_counter = book.pd - form = forms.SearchForm() - pubsuggest_form = PublishingSuggestForm( + form = PublishingSuggestForm( initial={"books": u"%s — %s, \n" % (book.author, book.title)}) return render_to_response('pdcounter/book_stub_detail.html', locals(), @@ -26,9 +24,8 @@ def book_stub_detail(request, slug): def author_detail(request, slug): author = get_object_or_404(models.Author, slug=slug) pd_counter = author.goes_to_pd() - form = forms.SearchForm() - pubsuggest_form = PublishingSuggestForm(initial={"books": author.name + ", \n"}) + form = PublishingSuggestForm(initial={"books": author.name + ", \n"}) return render_to_response('pdcounter/author_detail.html', locals(), context_instance=RequestContext(request)) diff --git a/apps/picture/__init__.py b/apps/picture/__init__.py new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/apps/picture/__init__.py @@ -0,0 +1 @@ + diff --git a/apps/picture/admin.py b/apps/picture/admin.py new file mode 100644 index 000000000..fb6bcf261 --- /dev/null +++ b/apps/picture/admin.py @@ -0,0 +1,9 @@ + +from django.contrib import admin +from picture.models import Picture +from sorl.thumbnail.admin import AdminImageMixin + +class PictureAdmin(AdminImageMixin, admin.ModelAdmin): + pass + +admin.site.register(Picture, PictureAdmin) diff --git a/apps/picture/forms.py b/apps/picture/forms.py new file mode 100644 index 000000000..ad5096bf5 --- /dev/null +++ b/apps/picture/forms.py @@ -0,0 +1,24 @@ +from django import forms +from django.utils.translation import ugettext_lazy as _ +from picture.models import Picture + + +class PictureImportForm(forms.Form): + picture_xml_file = forms.FileField(required=False) + picture_xml = forms.CharField(required=False) + picture_image_file = forms.FileField(required=True) + + def clean(self): + from django.core.files.base import ContentFile + + if not self.cleaned_data['picture_xml_file']: + if self.cleaned_data['picture_xml']: + self.cleaned_data['picture_xml_file'] = \ + ContentFile(self.cleaned_data['picture_xml'].encode('utf-8')) + else: + raise forms.ValidationError(_("Please supply an XML.")) + return super(PictureImportForm, self).clean() + + def save(self, commit=True, **kwargs): + return Picture.from_xml_file(self.cleaned_data['picture_xml_file'], image_file=self.cleaned_data['picture_image_file'], + overwrite=True, **kwargs) diff --git a/apps/picture/models.py b/apps/picture/models.py new file mode 100644 index 000000000..6ac54fe82 --- /dev/null +++ b/apps/picture/models.py @@ -0,0 +1,135 @@ +from django.db import models +import catalogue.models +from django.db.models import permalink +from sorl.thumbnail import ImageField +from django.conf import settings +from django.core.files.storage import FileSystemStorage +from django.utils.datastructures import SortedDict +from librarian import dcparser, picture + +from django.utils.translation import ugettext_lazy as _ +from newtagging import managers +from os import path + + +picture_storage = FileSystemStorage(location=path.join(settings.MEDIA_ROOT, 'pictures'), base_url=settings.MEDIA_URL + "pictures/") + + +class Picture(models.Model): + """ + Picture resource. + + """ + title = models.CharField(_('title'), max_length=120) + slug = models.SlugField(_('slug'), max_length=120, db_index=True, unique=True) + sort_key = models.CharField(_('sort key'), max_length=120, db_index=True, editable=False) + created_at = models.DateTimeField(_('creation date'), auto_now_add=True, db_index=True) + changed_at = models.DateTimeField(_('creation date'), auto_now=True, db_index=True) + xml_file = models.FileField('xml_file', upload_to="xml", storage=picture_storage) + image_file = ImageField(_('image_file'), upload_to="images", storage=picture_storage) + objects = models.Manager() + tagged = managers.ModelTaggedItemManager(catalogue.models.Tag) + tags = managers.TagDescriptor(catalogue.models.Tag) + + class AlreadyExists(Exception): + pass + + class Meta: + ordering = ('sort_key',) + + verbose_name = _('picture') + verbose_name_plural = _('pictures') + + URLID_RE = r'[a-z0-9-]+' + FILEID_RE = r'[a-z0-9-]+' + + def save(self, force_insert=False, force_update=False, reset_short_html=True, **kwargs): + from sortify import sortify + + self.sort_key = sortify(self.title) + + ret = super(Picture, self).save(force_insert, force_update) + + return ret + + def __unicode__(self): + return self.title + + @permalink + def get_absolute_url(self): + return ('picture.views.picture_detail', [self.urlid()]) + + def urlid(self): + return self.slug + + @classmethod + def from_xml_file(cls, xml_file, image_file=None, overwrite=False): + """ + Import xml and it's accompanying image file. + """ + from django.core.files import File + from librarian.picture import WLPicture + close_xml_file = False + + if not isinstance(xml_file, File): + xml_file = File(open(xml_file)) + close_xml_file = True + try: + # use librarian to parse meta-data + picture_xml = WLPicture.from_file(xml_file) + + picture, created = Picture.objects.get_or_create(slug=picture_xml.slug) + if not created and not overwrite: + raise Picture.AlreadyExists('Picture %s already exists' % picture_xml.slug) + + picture.title = picture_xml.picture_info.title + + picture.tags = catalogue.models.Tag.tags_from_info(picture_xml.picture_info) + + if image_file is not None: + img = image_file + else: + img = picture_xml.image_file() + + picture.image_file.save(path.basename(picture_xml.image_path), File(img)) + + picture.xml_file.save("%s.xml" % picture.slug, File(xml_file)) + picture.save() + finally: + if close_xml_file: + xml_file.close() + return picture + + @classmethod + def picture_list(cls, filter=None): + """Generates a hierarchical listing of all pictures + Pictures are optionally filtered with a test function. + """ + + pics = cls.objects.all().order_by('sort_key')\ + .only('title', 'slug', 'image_file') + + if filter: + pics = pics.filter(filter).distinct() + + pics_by_author = SortedDict() + orphans = [] + for tag in catalogue.models.Tag.objects.filter(category='author'): + pics_by_author[tag] = [] + + for pic in pics: + authors = list(pic.tags.filter(category='author')) + if authors: + for author in authors: + pics_by_author[author].append(pic) + else: + orphans.append(pic) + + return pics_by_author, orphans + + @property + def info(self): + if not hasattr(self, '_info'): + info = dcparser.parse(self.xml_file.path, picture.PictureInfo) + self._info = info + return self._info diff --git a/apps/picture/tests/__init__.py b/apps/picture/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/apps/picture/tests/files/kandinsky-composition-viii.png b/apps/picture/tests/files/kandinsky-composition-viii.png new file mode 100644 index 000000000..a809eb5c4 Binary files /dev/null and b/apps/picture/tests/files/kandinsky-composition-viii.png differ diff --git a/apps/picture/tests/files/kandinsky-composition-viii.xml b/apps/picture/tests/files/kandinsky-composition-viii.xml new file mode 100644 index 000000000..036bdf7e1 --- /dev/null +++ b/apps/picture/tests/files/kandinsky-composition-viii.xml @@ -0,0 +1,35 @@ + + + + Kandinsky, Vasily + composition 8 + Fundacja Nowoczesna Polska + Sekuła, Aleksandra + Kwiatkowska, Katarzyna + Trzeciak, Weronika + Modernizm + Obraz + Publikacja zrealizowana w ramach projektu Wolne Lektury (http://wolnelektury.pl). Reprodukcja cyfrowa wykonana przez Bibliotekę Narodową z egzemplarza pochodzącego ze zbiorów BN. + 55 1/8 x 79 1/8 inches + Olej na płótnie + http://wolnelektury.pl/katalog/obraz/kandinsky-composition-viii + http://www.ibiblio.org/wm/paint/auth/kandinsky/kandinsky.comp-8.jpg + Muzeum Narodowe, inw. 00000001. + Domena publiczna - Vasily Kandinsky zm. ? + 1940 + Image + image/png + 1090 x 755 px + 1923 + eng + + + + +
+ + +
+ + + diff --git a/apps/picture/views.py b/apps/picture/views.py new file mode 100644 index 000000000..c5be3bea3 --- /dev/null +++ b/apps/picture/views.py @@ -0,0 +1,31 @@ +from picture.models import Picture +from django.utils.datastructures import SortedDict +from django.shortcuts import render_to_response, get_object_or_404 +from django.template import RequestContext + + +def picture_list(request, filter=None, template_name='catalogue/picture_list.html'): + """ generates a listing of all books, optionally filtered with a test function """ + + pictures_by_author, orphans = Picture.picture_list() + books_nav = SortedDict() + for tag in pictures_by_author: + if pictures_by_author[tag]: + books_nav.setdefault(tag.sort_key[0], []).append(tag) + + # import pdb; pdb.set_trace() + return render_to_response(template_name, locals(), + context_instance=RequestContext(request)) + + +def picture_detail(request, picture): + picture = get_object_or_404(Picture, slug=picture) + + categories = SortedDict() + for tag in picture.tags: + categories.setdefault(tag.category, []).append(tag) + + picture_themes = [] + + return render_to_response("catalogue/picture_detail.html", locals(), + context_instance=RequestContext(request)) diff --git a/apps/reporting/__init__.py b/apps/reporting/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/apps/reporting/models.py b/apps/reporting/models.py new file mode 100644 index 000000000..740b9276e --- /dev/null +++ b/apps/reporting/models.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- +# This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later. +# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information. +# + + +# import views here, so that signals are attached correctly +from reporting.views import catalogue_pdf diff --git a/apps/reporting/templates/reporting/catalogue.texml b/apps/reporting/templates/reporting/catalogue.texml new file mode 100755 index 000000000..c17ed0754 --- /dev/null +++ b/apps/reporting/templates/reporting/catalogue.texml @@ -0,0 +1,118 @@ +{% load catalogue_tags %} + + + \documentclass[a4paper, oneside, 11pt]{book} + +\usepackage[MeX]{polski} + +\usepackage[xetex]{graphicx} +\usepackage{xunicode} +\usepackage{xltxtra} + +\usepackage{scalefnt} +\usepackage[colorlinks=true,linkcolor=black,setpagesize=false,urlcolor=black,xetex]{hyperref} + +\usepackage{longtable} + +\setmainfont [ +%ExternalLocation, +UprightFont = JunicodeWL-Regular, +ItalicFont = JunicodeWL-Italic, +BoldFont = JunicodeWL-Regular, +BoldItalicFont = JunicodeWL-Italic, +SmallCapsFont = JunicodeWL-Regular, +SmallCapsFeatures = {Letters={SmallCaps,UppercaseSmallCaps}}, +Numbers=OldStyle, +Scale=1.04, +LetterSpace=-1.0 +] {JunicodeWL} + +\pagestyle{plain} +\usepackage{fancyhdr} + +\makeatletter + +\usepackage{color} +\definecolor{note}{gray}{.3} + +\setlength{\hoffset}{-1cm} +\setlength{\oddsidemargin}{0pt} +\setlength{\marginparsep}{0pt} +\setlength{\marginparwidth}{0pt} + +\setlength{\voffset}{0pt} +\setlength{\topmargin}{0pt} +\setlength{\headheight}{0pt} +\setlength{\headsep}{0pt} +\setlength{\leftmargin}{0em} +\setlength{\rightmargin}{0em} +\setlength{\textheight}{24cm} +\setlength{\textwidth}{17.5cm} + + +\pagestyle{fancy} +\fancyhf{} +\renewcommand{\headrulewidth}{0pt} +\renewcommand{\footrulewidth}{0pt} +\lfoot{\footnotesize Katalog biblioteki internetowej WolneLektury.pl, \today} +\cfoot{} +\rfoot{\footnotesize \thepage} + +\clubpenalty=100000 +\widowpenalty=100000 + + +% see http://osdir.com/ml/tex.xetex/2005-10/msg00003.html +\newsavebox{\ximagebox}\newlength{\ximageheight} +\newsavebox{\xglyphbox}\newlength{\xglyphheight} +\newcommand{\xbox}[1] +{\savebox{\ximagebox}{#1}\settoheight{\ximageheight}{\usebox {\ximagebox}}% +\savebox{\xglyphbox}{\char32}\settoheight{\xglyphheight}{\usebox {\xglyphbox}}% +\raisebox{\ximageheight}[0pt][0pt]{%\raisebox{-\xglyphheight}[0pt] [0pt]{% +\makebox[0pt][l]{\usebox{\xglyphbox}}}%}% +\usebox{\ximagebox}% +\raisebox{0pt}[0pt][0pt]{\makebox[0pt][r]{\usebox{\xglyphbox}}}} + + +\newcommand{\name}[1]{% +\\ +\Large{#1}% +} + +\newcommand{\note}[1]{% +\small{\color{note}{#1}}% +} + + +\begin{document} + + \noindent \begin{minipage}[t]{.35\textwidth}\vspace{0pt} + \href{http://www.wolnelektury.pl}{\xbox{\includegraphics[width=\textwidth]{wl-logo.png}}} + \end{minipage} + + \begin{minipage}[t]{.65\textwidth}\vspace{0pt} + \begin{flushright} + \section*{Katalog biblioteki internetowej + \href{http://www.wolnelektury.pl/}{WolneLektury.pl}.} + stan na \today + \end{flushright} + \end{minipage} + + \begin{longtable}{p{9.5cm} p{5.5cm}r p{2cm}} + + + {% book_tree_texml orphans books_by_parent %} + {% for author, group in books_by_author.items %} + {% if group %} + {{ author }} + + + {% book_tree_texml group books_by_parent %} + {% endif %} + {% endfor %} + + + \end{longtable} + \end{document} + + \ No newline at end of file diff --git a/apps/reporting/templates/reporting/main.html b/apps/reporting/templates/reporting/main.html new file mode 100755 index 000000000..c629f105c --- /dev/null +++ b/apps/reporting/templates/reporting/main.html @@ -0,0 +1,36 @@ +{% extends "base.html" %} +{% load i18n %} +{% load reporting_stats catalogue_tags %} + +{% block titleextra %}{% trans "Reports" %}{% endblock %} + +{% block bodyid %}reports-stats{% endblock %} + + +{% block body %} +

Statystyka

+ +

Katalog biblioteki w formacie PDF.

+ + + + + + + + + + {% for mt in media_types %} + + + + + + {% endfor %} +
Utwory
Wszystkie utwory:{% count_books_all %}
Utwory z własną treścią:{% count_books_nonempty %}
Utwory bez własnej treści:{% count_books_empty %}
Niezależne książki:{% count_books_root %}
MediaLiczbaRozmiarDo wymiany
{{ mt.type }}:{{ mt.count }}{{ mt.size|filesizeformat }}{{ mt.deprecated }} + {% for m in mt.deprecated_files %} +
{{ m }} + {% endfor %} +
+ +{% endblock %} diff --git a/apps/reporting/templatetags/__init__.py b/apps/reporting/templatetags/__init__.py new file mode 100755 index 000000000..e69de29bb diff --git a/apps/reporting/templatetags/reporting_stats.py b/apps/reporting/templatetags/reporting_stats.py new file mode 100755 index 000000000..dceee0001 --- /dev/null +++ b/apps/reporting/templatetags/reporting_stats.py @@ -0,0 +1,61 @@ +# -*- coding: utf-8 -*- +# This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later. +# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information. +# +import feedparser +from functools import wraps +import datetime + +from django import template + +from catalogue.models import Book, BookMedia + + +register = template.Library() + +class StatsNode(template.Node): + def __init__(self, value, varname=None): + self.value = value + self.varname = varname + + def render(self, context): + if self.varname: + context[self.varname] = self.value + return '' + else: + return self.value + + +def register_counter(f): + """Turns a simple counting function into a registered counter tag. + + You can run a counter tag as a simple {% tag_name %} tag, or + as {% tag_name var_name %} to store the result in a variable. + + """ + @wraps(f) + def wrapped(parser, token): + try: + tag_name, args = token.contents.split(None, 1) + except ValueError: + args = None + return StatsNode(f(), args) + + return register.tag(wrapped) + + +@register_counter +def count_books_all(): + return Book.objects.all().count() + +@register_counter +def count_books_nonempty(): + return Book.objects.exclude(html_file='').count() + +@register_counter +def count_books_empty(): + return Book.objects.filter(html_file='').count() + +@register_counter +def count_books_root(): + return Book.objects.filter(parent=None).count() diff --git a/apps/reporting/urls.py b/apps/reporting/urls.py new file mode 100755 index 000000000..f50921513 --- /dev/null +++ b/apps/reporting/urls.py @@ -0,0 +1,12 @@ +# -*- coding: utf-8 -*- +# This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later. +# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information. +# +from django.conf.urls.defaults import * + + +urlpatterns = patterns('reporting.views', + url(r'^$', 'stats_page', name='reporting_stats'), + url(r'^katalog.pdf$', 'catalogue_pdf', name='reporting_catalogue_pdf'), +) + diff --git a/apps/reporting/utils.py b/apps/reporting/utils.py new file mode 100755 index 000000000..cc4e97a29 --- /dev/null +++ b/apps/reporting/utils.py @@ -0,0 +1,103 @@ +# -*- coding: utf-8 -*- +# This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later. +# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information. +# +from errno import ENOENT +import os +import os.path +from django.conf import settings +import logging +from django.http import HttpResponse + +logger = logging.getLogger(__name__) + + +def render_to_pdf(output_path, template, context=None, add_files=None): + """Renders a TeXML document into a PDF file. + + :param str output_path: is where the PDF file should go + :param str template: is a TeXML template path + :param context: is context for rendering the template + :param dict add_files: a dictionary of additional files XeTeX will need + """ + + from StringIO import StringIO + import shutil + from tempfile import mkdtemp + import subprocess + import Texml.processor + from django.template.loader import render_to_string + + rendered = render_to_string(template, context) + texml = StringIO(rendered.encode('utf-8')) + tempdir = mkdtemp(prefix = "render_to_pdf-") + tex_path = os.path.join(tempdir, "doc.tex") + with open(tex_path, 'w') as tex_file: + Texml.processor.process(texml, tex_file, encoding="utf-8") + + if add_files: + for add_name, src_file in add_files.items(): + add_path = os.path.join(tempdir, add_name) + if hasattr(src_file, "read"): + with open(add_path, 'w') as add_file: + add_file.write(add_file.read()) + else: + shutil.copy(src_file, add_path) + + cwd = os.getcwd() + os.chdir(tempdir) + try: + subprocess.check_call(['xelatex', '-interaction=batchmode', tex_path], + stdout=subprocess.PIPE, stderr=subprocess.PIPE) + try: + os.makedirs(os.path.dirname(output_path)) + except: + pass + shutil.move(os.path.join(tempdir, "doc.pdf"), output_path) + finally: + os.chdir(cwd) + shutil.rmtree(tempdir) + + +def read_chunks(f, size=8192): + chunk = f.read(size) + while chunk: + yield chunk + chunk = f.read(size) + + +def generated_file_view(file_name, mime_type, send_name=None, signals=None): + file_path = os.path.join(settings.MEDIA_ROOT, file_name) + file_url = os.path.join(settings.MEDIA_URL, file_name) + if send_name is None: + send_name = os.path.basename(file_name) + + def signal_handler(*args, **kwargs): + try: + os.unlink(file_path) + except OSError as oe: + if oe.errno != ENOENT: + raise oe + + if signals: + for signal in signals: + signal.connect(signal_handler, weak=False) + + def decorator(func): + def view(request, *args, **kwargs): + if not os.path.exists(file_path): + func(file_path, *args, **kwargs) + + if hasattr(send_name, "__call__"): + name = send_name() + else: + name = send_name + + response = HttpResponse(mimetype=mime_type) + response['Content-Disposition'] = 'attachment; filename=%s' % name + with open(file_path) as f: + for chunk in read_chunks(f): + response.write(chunk) + return response + return view + return decorator diff --git a/apps/reporting/views.py b/apps/reporting/views.py new file mode 100644 index 000000000..958e08009 --- /dev/null +++ b/apps/reporting/views.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- +# This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later. +# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information. +# +import os.path +from datetime import date +from django.conf import settings +from django.db.models import Count +from django.shortcuts import render_to_response +from django.template import RequestContext + +from catalogue.models import Book, BookMedia +from reporting.utils import render_to_pdf, generated_file_view + + +def stats_page(request): + media = BookMedia.objects.count() + media_types = BookMedia.objects.values('type').\ + annotate(count=Count('type')).\ + order_by('type') + for mt in media_types: + mt['size'] = sum(b.file.size for b in BookMedia.objects.filter(type=mt['type'])) + if mt['type'] in ('mp3', 'ogg'): + deprecated = BookMedia.objects.filter( + type=mt['type'], source_sha1=None) + mt['deprecated'] = deprecated.count() + mt['deprecated_files'] = deprecated.order_by('book', 'name') + else: + mt['deprecated'] = '-' + + return render_to_response('reporting/main.html', + locals(), context_instance=RequestContext(request)) + + +@generated_file_view('reports/katalog.pdf', 'application/pdf', + send_name=lambda: 'wolnelektury_%s.pdf' % date.today(), + signals=[Book.published]) +def catalogue_pdf(path): + books_by_author, orphans, books_by_parent = Book.book_list() + render_to_pdf(path, 'reporting/catalogue.texml', locals(), { + "wl-logo.png": os.path.join(settings.STATIC_ROOT, "img/logo-big.png"), + }) diff --git a/apps/sponsors/models.py b/apps/sponsors/models.py index d91c8f420..1e0d2e52d 100644 --- a/apps/sponsors/models.py +++ b/apps/sponsors/models.py @@ -9,24 +9,17 @@ from django.utils.translation import ugettext_lazy as _ from django.template.loader import render_to_string from PIL import Image -from sorl.thumbnail.fields import ImageWithThumbnailsField from sponsors.fields import JSONField from django.core.files.base import ContentFile -THUMB_WIDTH=120 -THUMB_HEIGHT=120 +THUMB_WIDTH = 120 +THUMB_HEIGHT = 120 + class Sponsor(models.Model): name = models.CharField(_('name'), max_length=120) _description = models.CharField(_('description'), blank=True, max_length=255) - logo = ImageWithThumbnailsField( - _('logo'), - upload_to='sponsorzy/sponsor/logo', - thumbnail={ - 'size': (THUMB_WIDTH, THUMB_HEIGHT), - 'extension': 'png', - 'options': ['pad', 'detail'], - }) + logo = models.ImageField(_('logo'), upload_to='sponsorzy/sponsor/logo') url = models.URLField(_('url'), blank=True, verify_exists=False) def __unicode__(self): @@ -65,10 +58,21 @@ class SponsorPage(models.Model): for column in self.get_sponsors_value(): sponsor_ids.extend(column['sponsors']) sponsors = Sponsor.objects.in_bulk(sponsor_ids) - sprite = Image.new('RGBA', (THUMB_WIDTH, len(sponsors)*THUMB_HEIGHT)) + sprite = Image.new('RGBA', (THUMB_WIDTH, len(sponsors) * THUMB_HEIGHT)) for i, sponsor_id in enumerate(sponsor_ids): - simg = Image.open(sponsors[sponsor_id].logo.thumbnail.dest) - sprite.paste(simg, (0, i*THUMB_HEIGHT)) + simg = Image.open(sponsors[sponsor_id].logo.path) + if simg.size[0] > THUMB_WIDTH or simg.size[1] > THUMB_HEIGHT: + size = ( + min(THUMB_WIDTH, + simg.size[0] * THUMB_HEIGHT / simg.size[1]), + min(THUMB_HEIGHT, + simg.size[1] * THUMB_WIDTH / simg.size[0]) + ) + simg = simg.resize(size, Image.ANTIALIAS) + sprite.paste(simg, ( + (THUMB_WIDTH - simg.size[0]) / 2, + i * THUMB_HEIGHT + (THUMB_HEIGHT - simg.size[1]) / 2, + )) imgstr = StringIO() sprite.save(imgstr, 'png') diff --git a/apps/sponsors/processors.py b/apps/sponsors/processors.py deleted file mode 100644 index 112241d96..000000000 --- a/apps/sponsors/processors.py +++ /dev/null @@ -1,18 +0,0 @@ -# -*- coding: utf-8 -*- -# This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later. -# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information. -# -from PIL import Image, ImageFilter, ImageChops - - -def add_padding(image, requested_size, opts): - if 'pad' in opts: - padded_image = Image.new('RGBA', requested_size, '#fff') - width, height = image.size - requested_width, requested_height = requested_size - print 'whatever' - padded_image.paste(image, (0, (requested_height - height) / 2)) - return padded_image - return image - -add_padding.valid_options = ('pad',) diff --git a/apps/sponsors/static/sponsors/css/footer_admin.css b/apps/sponsors/static/sponsors/css/footer_admin.css index 1277bd2d3..fe19d30c9 100644 --- a/apps/sponsors/static/sponsors/css/footer_admin.css +++ b/apps/sponsors/static/sponsors/css/footer_admin.css @@ -66,3 +66,8 @@ background-color: #EEE; cursor: default; } + +.sponsors-sponsor img { + max-width: 120px; + max-height: 120px; +} diff --git a/apps/sponsors/widgets.py b/apps/sponsors/widgets.py index e4b30bbbc..fc1387323 100644 --- a/apps/sponsors/widgets.py +++ b/apps/sponsors/widgets.py @@ -23,7 +23,7 @@ class SponsorPageWidget(forms.Textarea): def render(self, name, value, attrs=None): output = [super(SponsorPageWidget, self).render(name, value, attrs)] - sponsors = [(unicode(obj), obj.pk, obj.logo.thumbnail) for obj in models.Sponsor.objects.all()] + sponsors = [(unicode(obj), obj.pk, obj.logo.url) for obj in models.Sponsor.objects.all()] sponsors_js = ', '.join('{name: "%s", id: %d, image: "%s"}' % sponsor for sponsor in sponsors) output.append(u' - {% compressed_js "jquery" %} - {% compressed_js "all" %} + {% block extrahead %} {% endblock %} - + {% block bodycontent %} -
- {% chunk "top-message" %} -
-