Merge branch 'custompdf'
authorRadek Czajka <radoslaw.czajka@nowoczesnapolska.org.pl>
Thu, 29 Mar 2012 09:59:12 +0000 (11:59 +0200)
committerRadek Czajka <radoslaw.czajka@nowoczesnapolska.org.pl>
Thu, 29 Mar 2012 09:59:12 +0000 (11:59 +0200)
Conflicts:
wolnelektury/settings/__init__.py

44 files changed:
apps/ajaxable/templates/ajaxable/form.html
apps/ajaxable/utils.py
apps/catalogue/forms.py
apps/catalogue/models.py
apps/catalogue/tasks.py
apps/catalogue/templates/catalogue/book_short.html
apps/catalogue/templates/catalogue/book_wide.html
apps/catalogue/templatetags/catalogue_tags.py
apps/catalogue/tests/book_import.py
apps/catalogue/tests/tags.py
apps/catalogue/urls.py
apps/catalogue/utils.py
apps/catalogue/views.py
apps/dictionary/models.py
apps/picture/tests/picture_import.py
apps/social/templates/social/sets_form.html
apps/suggest/templates/publishing_suggest.html
apps/suggest/views.py
apps/waiter/__init__.py [new file with mode: 0644]
apps/waiter/locale/pl/LC_MESSAGES/django.mo [new file with mode: 0644]
apps/waiter/locale/pl/LC_MESSAGES/django.po [new file with mode: 0644]
apps/waiter/migrations/0001_initial.py [new file with mode: 0644]
apps/waiter/migrations/__init__.py [new file with mode: 0644]
apps/waiter/models.py [new file with mode: 0644]
apps/waiter/settings.py [new file with mode: 0644]
apps/waiter/tasks.py [new file with mode: 0644]
apps/waiter/templates/waiter/wait.html [new file with mode: 0644]
apps/waiter/urls.py [new file with mode: 0644]
apps/waiter/utils.py [new file with mode: 0644]
apps/waiter/views.py [new file with mode: 0644]
apps/wolnelektury_core/templates/admin/base_site.html
apps/wolnelektury_core/templates/admin/catalogue/book/change_list.html
apps/wolnelektury_core/templates/auth/login.html
apps/wolnelektury_core/templates/auth/login_register.html
apps/wolnelektury_core/templates/piston/authorize_token.html
apps/wolnelektury_core/templates/superbase.html
apps/wolnelektury_core/views.py
lib/librarian
requirements.txt
wolnelektury/settings/__init__.py
wolnelektury/settings/celery.py
wolnelektury/settings/contrib.py [new file with mode: 0644]
wolnelektury/settings/custom.py
wolnelektury/urls.py

index 84e86e1..38113db 100755 (executable)
@@ -1,8 +1,14 @@
 {% load i18n %}
+
 <h1>{{ title }}</h1>
 
 <form action="{{ request.get_full_path }}" method="post" accept-charset="utf-8"
        class="cuteform{% if placeholdize %} hidelabels{% endif %}">
+{% csrf_token %}
+{% if honeypot %}
+    {% load honeypot %}
+    {% render_honeypot_field %}
+{% endif %}
 <ol>
     <div id="id_{% if form_prefix %}{{ form_prefix }}-{% endif %}__all__"></div>
     {{ form.as_ul }}
index 02e8767..4ae6e86 100755 (executable)
@@ -10,6 +10,7 @@ from django.utils.http import urlquote_plus
 from django.utils import simplejson
 from django.utils.translation import ugettext_lazy as _
 from django.views.decorators.vary import vary_on_headers
+from honeypot.decorators import verify_honeypot_value
 
 
 class LazyEncoder(simplejson.JSONEncoder):
@@ -76,6 +77,7 @@ class AjaxableFormView(object):
     formname = "form"
     form_prefix = None
     full_template = "ajaxable/form_on_page.html"
+    honeypot = False
 
     @method_decorator(vary_on_headers('X-Requested-With'))
     def __call__(self, request, *args, **kwargs):
@@ -86,6 +88,11 @@ class AjaxableFormView(object):
             form_kwargs['prefix'] = self.form_prefix
 
         if request.method == "POST":
+            if self.honeypot:
+                response = verify_honeypot_value(request, None)
+                if response:
+                    return response
+
             # do I need to be logged in?
             if self.POST_login and not request.user.is_authenticated():
                 return require_login(request)
@@ -94,14 +101,16 @@ class AjaxableFormView(object):
             form = self.form_class(*form_args, **form_kwargs)
             if form.is_valid():
                 add_args = self.success(form, request)
-                redirect = request.GET.get('next')
-                if not request.is_ajax() and redirect:
-                    return HttpResponseRedirect(urlquote_plus(
-                            redirect, safe='/?=&'))
-                response_data = {'success': True, 
-                    'message': self.success_message, 'redirect': redirect}
+                response_data = {
+                    'success': True, 
+                    'message': self.success_message,
+                    'redirect': request.GET.get('next')
+                    }
                 if add_args:
                     response_data.update(add_args)
+                if not request.is_ajax() and response_data['redirect']:
+                    return HttpResponseRedirect(urlquote_plus(
+                            response_data['redirect'], safe='/?=&'))
             elif request.is_ajax():
                 # Form was sent with errors. Send them back.
                 if self.form_prefix:
@@ -136,6 +145,7 @@ class AjaxableFormView(object):
         context = {
                 self.formname: form, 
                 "title": title,
+                "honeypot": self.honeypot,
                 "placeholdize": self.placeholdize,
                 "submit": self.submit,
                 "response_data": response_data,
index 9115b2d..c4ddbcb 100644 (file)
@@ -6,6 +6,10 @@ from django import forms
 from django.utils.translation import ugettext_lazy as _
 
 from catalogue.models import Book
+from waiter.models import WaitedFile
+from django.core.exceptions import ValidationError
+from catalogue.utils import get_customized_pdf_path
+from catalogue.tasks import build_custom_pdf
 
 
 class BookImportForm(forms.Form):
@@ -38,51 +42,61 @@ class DownloadFormatsForm(forms.Form):
         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')),
+CUSTOMIZATION_FLAGS = (
+    ('nofootnotes', _("Don't show footnotes")),
+    ('nothemes', _("Don't disply themes")),
+    ('nowlfont', _("Don't use our custom font")),
     )
-
-PDF_FONT_SIZES = (
-    ('11pt', _('Default')),
-    ('13pt', _('Big'))
+CUSTOMIZATION_OPTIONS = (
+    ('leading', _("Leading"), (
+        ('defaultleading', _('Normal leading')),
+        ('onehalfleading', _('One and a half leading')),
+        ('doubleleading', _('Double leading')),
+        )),
+    ('fontsize', _("Font size"), (
+        ('11pt', _('Default')),
+        ('13pt', _('Big'))
+        )),
+#    ('pagesize', _("Paper size"), (
+#        ('a4paper', _('A4')),
+#        ('a5paper', _('A5')),
+#        )),
     )
 
 
 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"))
+    def __init__(self, book, *args, **kwargs):
+        super(CustomPDFForm, self).__init__(*args, **kwargs)
+        self.book = book
+        for name, label in CUSTOMIZATION_FLAGS:
+            self.fields[name] = forms.BooleanField(required=False, label=label)
+        for name, label, choices in CUSTOMIZATION_OPTIONS:
+            self.fields[name] = forms.ChoiceField(choices, label=label)
+
+    def clean(self):
+        self.cleaned_data['cust'] = self.customizations
+        self.cleaned_data['path'] = get_customized_pdf_path(self.book,
+            self.cleaned_data['cust'])
+        if not WaitedFile.can_order(self.cleaned_data['path']):
+            raise ValidationError(_('Queue is full. Please try again later.'))
+        return self.cleaned_data
 
     @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'])
-
+        for name, label in CUSTOMIZATION_FLAGS:
+            if self.cleaned_data.get(name):
+                c.append(name)
+        for name, label, choices in CUSTOMIZATION_OPTIONS:
+            c.append(self.cleaned_data[name])
         c.sort()
-
         return c
 
+    def save(self, *args, **kwargs):
+        url = WaitedFile.order(self.cleaned_data['path'],
+            lambda p: build_custom_pdf.delay(self.book.id,
+                self.cleaned_data['cust'], p),
+            self.book.pretty_title()
+            )
+        #return redirect(url)
+        return {"redirect": url}
index ea09c04..ba1a5d2 100644 (file)
@@ -5,10 +5,9 @@
 from collections import namedtuple
 
 from django.db import models
-from django.db.models import permalink, Q
+from django.db.models import permalink
 import django.dispatch
 from django.core.cache import get_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.template.loader import render_to_string
@@ -16,7 +15,7 @@ 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
-from django.db.models.signals import post_save, m2m_changed, pre_delete, post_delete
+from django.db.models.signals import post_save, pre_delete, post_delete
 import jsonfield
 
 from django.conf import settings
@@ -25,11 +24,8 @@ from newtagging.models import TagBase, tags_updated
 from newtagging import managers
 from catalogue.fields import JSONField, OverwritingFileField
 from catalogue.utils import create_zip, split_tags, truncate_html_words
-from catalogue.tasks import touch_tag, index_book
-from shutil import copy
-from glob import glob
+from catalogue import tasks
 import re
-from os import path
 
 import search
 
@@ -216,32 +212,6 @@ def book_upload_path(ext=None, maxlen=100):
     return lambda *args: get_dynamic_path(*args, ext=ext, maxlen=maxlen)
 
 
-def customizations_hash(customizations):
-    customizations.sort()
-    return hash(tuple(customizations))
-
-
-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.
-    """
-    h = customizations_hash(customizations)
-    pdf_name = '%s-custom-%s' % (book.slug, 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.slug,)
-    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):
     FileFormat = namedtuple("FileFormat", "name ext")
     formats = SortedDict([
@@ -274,7 +244,7 @@ class BookMedia(models.Model):
 
         try:
             old = BookMedia.objects.get(pk=self.pk)
-        except BookMedia.DoesNotExist, e:
+        except BookMedia.DoesNotExist:
             old = None
         else:
             # if name changed, change the file name, too
@@ -428,18 +398,18 @@ class Book(models.Model):
             book_tag.save()
         return book_tag
 
-    def has_media(self, type):
-        if type in Book.formats:
-            return bool(getattr(self, "%s_file" % type))
+    def has_media(self, type_):
+        if type_ in Book.formats:
+            return bool(getattr(self, "%s_file" % type_))
         else:
-            return self.media.filter(type=type).exists()
+            return self.media.filter(type=type_).exists()
 
-    def get_media(self, type):
-        if self.has_media(type):
-            if type in Book.formats:
-                return getattr(self, "%s_file" % type)
+    def get_media(self, type_):
+        if self.has_media(type_):
+            if type_ in Book.formats:
+                return getattr(self, "%s_file" % type_)
             else:                                             
-                return self.media.filter(type=type)
+                return self.media.filter(type=type_)
         else:
             return None
 
@@ -504,69 +474,6 @@ class Book(models.Model):
         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 os import unlink
-        from django.core.files import File
-        from catalogue.utils import remove_zip
-
-        pdf = self.wldocument().as_pdf(customizations=customizations)
-
-        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.slug,
-                    File(open(pdf.get_filename())))
-            self.pdf_file = current_self.pdf_file
-
-            # remove cached downloadables
-            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 django.core.files import File
-        from catalogue.utils import remove_zip
-
-        mobi = self.wldocument().as_mobi()
-
-        self.mobi_file.save('%s.mobi' % self.slug, File(open(mobi.get_filename())))
-
-        # remove zip with all mobi files
-        remove_zip(settings.ALL_MOBI_ZIP)
-
-    def build_epub(self):
-        """(Re)builds the epub file."""
-        from django.core.files import File
-        from catalogue.utils import remove_zip
-
-        epub = self.wldocument().as_epub()
-
-        self.epub_file.save('%s.epub' % self.slug,
-                File(open(epub.get_filename())))
-
-        # remove zip package with all epub files
-        remove_zip(settings.ALL_EPUB_ZIP)
-
-    def build_txt(self):
-        from django.core.files.base import ContentFile
-
-        text = self.wldocument().as_text()
-        self.txt_file.save('%s.txt' % self.slug, ContentFile(text.get_string()))
-
-
     def build_html(self):
         from django.core.files.base import ContentFile
         from slughifi import slughifi
@@ -624,6 +531,16 @@ class Book(models.Model):
             return True
         return False
 
+    # Thin wrappers for builder tasks
+    def build_pdf(self, *args, **kwargs):
+        return tasks.build_pdf.delay(self.pk, *args, **kwargs)
+    def build_epub(self, *args, **kwargs):
+        return tasks.build_epub.delay(self.pk, *args, **kwargs)
+    def build_mobi(self, *args, **kwargs):
+        return tasks.build_mobi.delay(self.pk, *args, **kwargs)
+    def build_txt(self, *args, **kwargs):
+        return tasks.build_txt.delay(self.pk, *args, **kwargs)
+
     @staticmethod
     def zip_format(format_):
         def pretty_file_name(book):
@@ -636,15 +553,13 @@ class Book(models.Model):
         books = Book.objects.filter(parent=None).exclude(**{field_name: ""})
         paths = [(pretty_file_name(b), getattr(b, field_name).path)
                     for b in books]
-        result = create_zip.delay(paths,
+        return create_zip(paths,
                     getattr(settings, "ALL_%s_ZIP" % format_.upper()))
-        return result.wait()
 
     def zip_audiobooks(self, format_):
         bm = BookMedia.objects.filter(book=self, type=format_)
         paths = map(lambda bm: (None, bm.file.path), bm)
-        result = create_zip.delay(paths, "%s_%s" % (self.slug, format_))
-        return result.wait()
+        return create_zip(paths, "%s_%s" % (self.slug, format_))
 
     def search_index(self, book_info=None, reuse_index=False, index_tags=True):
         if reuse_index:
@@ -680,8 +595,6 @@ class Book(models.Model):
     def from_text_and_meta(cls, raw_file, book_info, overwrite=False,
             build_epub=True, build_txt=True, build_pdf=True, build_mobi=True,
             search_index=True, search_index_tags=True, search_index_reuse=False):
-        import re
-        from sortify import sortify
 
         # check for parts before we do anything
         children = []
@@ -689,7 +602,7 @@ class Book(models.Model):
             for part_url in book_info.parts:
                 try:
                     children.append(Book.objects.get(slug=part_url.slug))
-                except Book.DoesNotExist, e:
+                except Book.DoesNotExist:
                     raise Book.DoesNotExist(_('Book "%s" does not exist.') %
                             part_url.slug)
 
@@ -767,7 +680,7 @@ class Book(models.Model):
             book_descendants += list(child_book.children.all())
 
         for tag in descendants_tags:
-            touch_tag(tag)
+            tasks.touch_tag(tag)
 
         book.save()
 
@@ -808,6 +721,13 @@ class Book(models.Model):
                 type(self).objects.filter(pk=self.pk).update(_related_info=rel)
             return rel
 
+    def related_themes(self):
+        theme_counter = self.theme_counter
+        book_themes = Tag.objects.filter(pk__in=theme_counter.keys())
+        for tag in book_themes:
+            tag.count = theme_counter[tag.pk]
+        return book_themes
+
     def reset_tag_counter(self):
         if self.id is None:
             return
@@ -1055,7 +975,7 @@ 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
     for tag in affected_tags:
-        touch_tag(tag)
+        tasks.touch_tag(tag)
 
     # if book tags changed, reset book tag counter
     if isinstance(sender, Book) and \
index e1ff915..6d19ee1 100755 (executable)
@@ -3,11 +3,12 @@
 # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
 #
 from datetime import datetime
-from celery.task import task
-import catalogue.models
 from traceback import print_exc
+from celery.task import task
+from django.conf import settings
 
-@task
+
+# TODO: move to model?
 def touch_tag(tag):
     update_dict = {
         'book_count': tag.get_count(),
@@ -19,9 +20,97 @@ def touch_tag(tag):
 
 @task
 def index_book(book_id, book_info=None):
+    from catalogue.models import Book
     try:
-        return catalogue.models.Book.objects.get(id=book_id).search_index(book_info)
+        return Book.objects.get(id=book_id).search_index(book_info)
     except Exception, e:
         print "Exception during index: %s" % e
         print_exc()
         raise e
+
+
+@task(ignore_result=True)
+def build_txt(book_id):
+    """(Re)builds the TXT file for a book."""
+    from django.core.files.base import ContentFile
+    from catalogue.models import Book
+
+    text = Book.objects.get(pk=book_id).wldocument().as_text()
+
+    # Save the file in new instance. Building TXT takes time and we don't want
+    # to overwrite any interim changes.
+    book = Book.objects.get(id=book_id)
+    book.txt_file.save('%s.txt' % book.slug, ContentFile(text.get_string()))
+
+
+@task(ignore_result=True, rate_limit=settings.CATALOGUE_PDF_RATE_LIMIT)
+def build_pdf(book_id):
+    """(Re)builds the pdf file for a book."""
+    from django.core.files import File
+    from catalogue.models import Book
+    from catalogue.utils import remove_zip
+    from waiter.utils import clear_cache
+
+    pdf = Book.objects.get(pk=book_id).wldocument().as_pdf(
+            morefloats=settings.LIBRARIAN_PDF_MOREFLOATS)
+
+    # Save the file in new instance. Building PDF takes time and we don't want
+    # to overwrite any interim changes.
+    book = Book.objects.get(id=book_id)
+    book.pdf_file.save('%s.pdf' % book.slug,
+             File(open(pdf.get_filename())))
+
+    # Remove cached downloadables
+    remove_zip(settings.ALL_PDF_ZIP)
+    clear_cache(book.slug)
+
+
+@task(ignore_result=True, rate_limit=settings.CATALOGUE_EPUB_RATE_LIMIT)
+def build_epub(book_id):
+    """(Re)builds the EPUB file for a book."""
+    from django.core.files import File
+    from catalogue.models import Book
+    from catalogue.utils import remove_zip
+
+    epub = Book.objects.get(pk=book_id).wldocument().as_epub()
+    # Save the file in new instance. Building MOBI takes time and we don't want
+    # to overwrite any interim changes.
+    book = Book.objects.get(id=book_id)
+    book.epub_file.save('%s.epub' % book.slug,
+             File(open(epub.get_filename())))
+
+    # remove zip with all epub files
+    remove_zip(settings.ALL_EPUB_ZIP)
+
+
+@task(ignore_result=True, rate_limit=settings.CATALOGUE_MOBI_RATE_LIMIT)
+def build_mobi(book_id):
+    """(Re)builds the MOBI file for a book."""
+    from django.core.files import File
+    from catalogue.models import Book
+    from catalogue.utils import remove_zip
+
+    mobi = Book.objects.get(pk=book_id).wldocument().as_mobi()
+    # Save the file in new instance. Building MOBI takes time and we don't want
+    # to overwrite any interim changes.
+    book = Book.objects.get(id=book_id)
+    book.mobi_file.save('%s.mobi' % book.slug,
+             File(open(mobi.get_filename())))
+
+    # remove zip with all mobi files
+    remove_zip(settings.ALL_MOBI_ZIP)
+
+
+@task(rate_limit=settings.CATALOGUE_CUSTOMPDF_RATE_LIMIT)
+def build_custom_pdf(book_id, customizations, file_name):
+    """Builds a custom PDF file."""
+    from django.core.files import File
+    from django.core.files.storage import DefaultStorage
+    from catalogue.models import Book
+
+    print "will gen %s" % DefaultStorage().path(file_name)
+    if not DefaultStorage().exists(file_name):
+        pdf = Book.objects.get(pk=book_id).wldocument().as_pdf(
+                customizations=customizations,
+                morefloats=settings.LIBRARIAN_PDF_MOREFLOATS)
+        DefaultStorage().save(file_name, File(open(pdf.get_filename())))
index 1b1eb4b..d9b5b76 100644 (file)
@@ -27,6 +27,7 @@
     </div>
     <div class="if-unlike">
         <form id="social-like-book-{{ book.slug }}" data-callback='social-like-book' method='post' class='ajax-form' action='{% url social_like_book book.slug %}'>
+            {% csrf_token %}
             <button type='submit'>☆</button>
         </form>
     </div>
index c683fce..ad588bd 100644 (file)
@@ -52,7 +52,6 @@
     </ul>
   </div>
   <div class="other-download">
-       {% if related.media.mp3 or related.media.ogg %}
     <h2 class="mono">{% trans "Download" %}</h2>
     <ul class="plain">
       <li>
        {% if related.media.ogg %}<a href="{% url download_zip_ogg book.slug %}">OGG</a>{% endif %}.
        {% endif %}
       </li>
-      {% comment %}
       <li>
-       <a href="{% url custom_pdf_form %}?slug={{book.slug}}" id="custom-pdf" class="ajaxable">{% trans "Download a custom PDF" %}</a>
+       <a href="{% url custom_pdf_form book.slug %}" id="custom-pdf" class="ajaxable">{% trans "Download a custom PDF" %}</a>
       </li>
-      {% endcomment %}
     </ul>
-    {% endif %}
   </div>
 </div>
 {% endblock %}
index 7d40128..e5e4d4f 100644 (file)
@@ -295,10 +295,7 @@ def book_info(book):
 
 @register.inclusion_tag('catalogue/book_wide.html', takes_context=True)
 def book_wide(context, book):
-    theme_counter = book.theme_counter
-    book_themes = Tag.objects.filter(pk__in=theme_counter.keys())
-    for tag in book_themes:
-        tag.count = theme_counter[tag.pk]
+    book_themes = book.related_themes()
     extra_info = book.get_extra_info_value()
     hide_about = extra_info.get('about', '').startswith('http://wiki.wolnepodreczniki.pl')
 
@@ -309,7 +306,6 @@ def book_wide(context, book):
         'extra_info': book.get_extra_info_value(),
         'hide_about': hide_about,
         'themes': book_themes,
-        'custom_pdf_form': forms.CustomPDFForm(),
         'request': context.get('request'),
     }
 
index 3af1bb4..6ece328 100644 (file)
@@ -7,8 +7,7 @@ from catalogue import models
 from librarian import WLURI
 
 from nose.tools import raises
-import tempfile
-from os import unlink, path, makedirs
+from os import path, makedirs
 
 class BookImportLogicTests(WLTestCase):
 
@@ -237,9 +236,7 @@ class ChildImportTests(WLTestCase):
         </opowiadanie></utwor>
         """
         child = models.Book.from_text_and_meta(ContentFile(CHILD_TEXT), self.child_info, overwrite=True)
-
-        themes = self.client.get(parent.get_absolute_url()).context['book_themes']
-
+        themes = parent.related_themes()
         self.assertEqual(['Kot'], [tag.name for tag in themes],
                         'wrong related theme list')
 
@@ -283,26 +280,30 @@ class MultilingualBookImportTest(WLTestCase):
 class BookImportGenerateTest(WLTestCase):
     def setUp(self):
         WLTestCase.setUp(self)
-        input = path.join(path.dirname(__file__), 'files/fraszka-do-anusie.xml')
-        self.book = models.Book.from_xml_file(input)
+        xml = path.join(path.dirname(__file__), 'files/fraszka-do-anusie.xml')
+        self.book = models.Book.from_xml_file(xml)
 
     def test_gen_pdf(self):
         self.book.build_pdf()
-        self.assertTrue(path.exists(self.book.pdf_file.path))
+        book = models.Book.objects.get(pk=self.book.pk)
+        self.assertTrue(path.exists(book.pdf_file.path))
 
     def test_gen_pdf_parent(self):
         """This book contains a child."""
-        input = path.join(path.dirname(__file__), "files/fraszki.xml")
-        parent = models.Book.from_xml_file(input)
+        xml = path.join(path.dirname(__file__), "files/fraszki.xml")
+        parent = models.Book.from_xml_file(xml)
         parent.build_pdf()
+        parent = models.Book.objects.get(pk=parent.pk)
         self.assertTrue(path.exists(parent.pdf_file.path))
 
     def test_custom_pdf(self):
+        from catalogue.tasks import build_custom_pdf
         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)
+        build_custom_pdf(self.book.id,
+            customizations=['nofootnotes', '13pt', 'a4paper'], file_name=out)
         self.assertTrue(path.exists(absoulute_path))
index a47e426..3eab3da 100644 (file)
@@ -1,7 +1,9 @@
 # -*- coding: utf-8 -*-
+from django.core.files.base import ContentFile
+from django.test import Client
 from catalogue import models
 from catalogue.test_utils import *
-from django.core.files.base import ContentFile
+
 
 class BooksByTagTests(WLTestCase):
     """ tests the /katalog/category/tag page for found books """
@@ -63,7 +65,6 @@ class BooksByTagTests(WLTestCase):
                          ['Child'])
 
 
-from django.test import Client
 class TagRelatedTagsTests(WLTestCase):
     """ tests the /katalog/category/tag/ page for related tags """
 
@@ -173,7 +174,7 @@ class CleanTagRelationTests(WLTestCase):
             <end id="m01" />
             </akap></opowiadanie></utwor>
             """
-        book = models.Book.from_text_and_meta(ContentFile(book_text), book_info)
+        self.book = models.Book.from_text_and_meta(ContentFile(book_text), book_info)
 
     def test_delete_objects(self):
         """ there should be no related tags left after deleting some objects """
@@ -190,8 +191,8 @@ class CleanTagRelationTests(WLTestCase):
         """ there should be no tag relations left after deleting tags """
 
         models.Tag.objects.all().delete()
-        cats = self.client.get('/katalog/lektura/book/').context['categories']
-        self.assertEqual(cats, {})
+        self.assertEqual(len(self.book.related_info()['tags']), 0)
+        self.assertEqual(len(self.book.related_themes()), 0)
         self.assertEqual(models.Tag.intermediary_table_model.objects.all().count(), 0,
                          "orphaned TagRelation objects left")
 
@@ -219,13 +220,14 @@ class TestIdenticalTag(WLTestCase):
 
     def test_book_tags(self):
         """ there should be all related tags in relevant categories """
-        models.Book.from_text_and_meta(ContentFile(self.book_text), self.book_info)
+        book = models.Book.from_text_and_meta(ContentFile(self.book_text), self.book_info)
 
-        context = self.client.get('/katalog/lektura/tag/').context
+        related_info = book.related_info()
+        related_themes = book.related_themes()
         for category in 'author', 'kind', 'genre', 'epoch':
-            self.assertTrue('tag' in [tag.slug for tag in context['categories'][category]],
+            self.assertTrue('tag' in [tag[1] for tag in related_info['tags'][category]],
                             'missing related tag for %s' % category)
-        self.assertTrue('tag' in [tag.slug for tag in context['book_themes']])
+        self.assertTrue('tag' in [tag.slug for tag in related_themes])
 
     def test_qualified_url(self):
         models.Book.from_text_and_meta(ContentFile(self.book_text), self.book_info)
@@ -259,27 +261,28 @@ class BookTagsTests(WLTestCase):
                 <end id="m01" />
                 </akap></opowiadanie></utwor>
                 """ % info.title.encode('utf-8')
-            book = models.Book.from_text_and_meta(ContentFile(book_text), info)
+            models.Book.from_text_and_meta(ContentFile(book_text), info)
 
     def test_book_tags(self):
         """ book should have own tags and whole tree's themes """
 
-        context = self.client.get('/katalog/lektura/parent/').context
+        book = models.Book.objects.get(slug='parent')
+        related_info = book.related_info()
+        related_themes = book.related_themes()
 
-        self.assertEqual([tag.name for tag in context['categories']['author']],
-                         ['Common Man'])
-        self.assertEqual([tag.name for tag in context['categories']['kind']],
-                         ['Kind'])
-        self.assertEqual([(tag.name, tag.count) for tag in context['book_themes']],
+        self.assertEqual(related_info['tags']['author'],
+                         [('Common Man', 'common-man')])
+        self.assertEqual(related_info['tags']['kind'],
+                         [('Kind', 'kind')])
+        self.assertEqual([(tag.name, tag.count) for tag in related_themes],
                          [('ChildTheme', 1), ('ParentTheme', 1), ('Theme', 2)])
 
     def test_main_page_tags(self):
         """ test main page tags and counts """
-
-        context = self.client.get('/katalog/').context
-
-        self.assertEqual([(tag.name, tag.count) for tag in context['categories']['author']],
+        from catalogue.templatetags.catalogue_tags import catalogue_menu
+        menu = catalogue_menu()
+        self.assertEqual([(tag.name, tag.book_count) for tag in menu['author']],
                          [('Jim Lazy', 1), ('Common Man', 1)])
-        self.assertEqual([(tag.name, tag.count) for tag in context['fragment_tags']],
+        self.assertEqual([(tag.name, tag.book_count) for tag in menu['theme']],
                          [('ChildTheme', 1), ('ParentTheme', 1), ('Theme', 2)])
 
index da4a3ae..647bc9f 100644 (file)
@@ -37,6 +37,11 @@ urlpatterns += patterns('catalogue.views',
     url(r'^jtags/$', 'json_tags_starting_with', name='jhint'),
     #url(r'^szukaj/$', 'search', name='old_search'),
 
+    url(r'^custompdf/(?P<slug>%s)/$' % SLUG, CustomPDFFormView(), name='custom_pdf_form'),
+
+    url(r'^audiobooki/(?P<type>mp3|ogg|daisy|all).xml$', AudiobookFeed(), name='audiobook_feed'),
+
+
     # zip
     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'),
@@ -51,11 +56,6 @@ urlpatterns += patterns('catalogue.views',
     url(r'^lektura/(?P<slug>%s)/motyw/(?P<theme_slug>[a-zA-Z0-9-]+)/$' % SLUG,
         'book_fragments', name='book_fragments'),
 
+    # This should be the last pattern.
     url(r'^(?P<tags>[a-zA-Z0-9-/]*)/$', 'tagged_object_list', name='tagged_object_list'),
-
-    url(r'^audiobooki/(?P<type>mp3|ogg|daisy|all).xml$', AudiobookFeed(), name='audiobook_feed'),
-
-    url(r'^custompdf$', CustomPDFFormView(), name='custom_pdf_form'),
-    url(r'^custompdf/(?P<slug>%s).pdf' % SLUG, 'download_custom_pdf'),
-
 )
index a8a12e5..29f40d1 100644 (file)
@@ -9,23 +9,18 @@ import re
 import time
 from base64 import urlsafe_b64encode
 
-from django.http import HttpResponse, HttpResponseRedirect, Http404, HttpResponsePermanentRedirect
+from django.http import HttpResponse
 from django.core.files.uploadedfile import UploadedFile
-from django.core.files.base import File
 from django.core.files.storage import DefaultStorage
 from django.utils.encoding import force_unicode
 from django.utils.hashcompat import sha_constructor
 from django.conf import settings
-from celery.task import task
 from os import mkdir, path, unlink
 from errno import EEXIST, ENOENT
 from fcntl import flock, LOCK_EX
 from zipfile import ZipFile
-from traceback import print_exc
 
 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'):
@@ -84,7 +79,7 @@ class LockFile(object):
         self.lock.close()
 
 
-@task
+#@task
 def create_zip(paths, zip_slug):
     """
     Creates a zip in MEDIA_ROOT/zip directory containing files from path.
@@ -140,25 +135,6 @@ class AttachmentHttpResponse(HttpResponse):
             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
-    """
-    try:
-        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."
-    except Exception, e:
-        print "Error during pdf creation: %s" % e
-        print_exc
-        raise e
-
-
 class MultiQuerySet(object):
     def __init__(self, *args, **kwargs):
         self.querysets = args
@@ -260,3 +236,24 @@ def truncate_html_words(s, num, end_text='...'):
         out += '</%s>' % tag
     # Return string
     return out
+
+
+def customizations_hash(customizations):
+    customizations.sort()
+    return hash(tuple(customizations))
+
+
+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.
+    """
+    h = customizations_hash(customizations)
+    return 'book/%s/%s-custom-%s.pdf' % (book.slug, book.slug, h)
+
+
+def clear_custom_pdf(book):
+    """
+    Returns a list of paths to generated customized pdf of a book
+    """
+    from waiter.utils import clear_cache
+    clear_cache('book/%s' % book.slug)
index 7ead471..d2176bf 100644 (file)
@@ -7,7 +7,7 @@ import itertools
 
 from django.conf import settings
 from django.template import RequestContext
-from django.shortcuts import render_to_response, get_object_or_404
+from django.shortcuts import render_to_response, get_object_or_404, redirect
 from django.http import HttpResponse, HttpResponseRedirect, Http404, HttpResponsePermanentRedirect
 from django.core.urlresolvers import reverse
 from django.db.models import Q
@@ -22,15 +22,12 @@ from ajaxable.utils import JSONResponse, AjaxableFormView
 
 from catalogue import models
 from catalogue import forms
-from catalogue.utils import (split_tags, AttachmentHttpResponse,
-    async_build_pdf, MultiQuerySet)
+from catalogue.utils import split_tags, MultiQuerySet
 from pdcounter import models as pdcounter_models
 from pdcounter import views as pdcounter_views
 from suggest.forms import PublishingSuggestForm
 from picture.models import Picture
 
-from os import path
-
 staff_required = user_passes_test(lambda user: user.is_staff)
 
 
@@ -531,43 +528,18 @@ def download_zip(request, format, slug=None):
     return HttpResponseRedirect(urlquote_plus(settings.MEDIA_URL + url, safe='/?='))
 
 
-def download_custom_pdf(request, slug, method='GET'):
-    book = get_object_or_404(models.Book, slug=slug)
-
-    if request.method == method:
-        form = forms.CustomPDFForm(method == 'GET' and request.GET or request.POST)
-        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.slug), file_path=pdf_file, mimetype="application/pdf")
-        else:
-            raise Http404(_('Incorrect customization options for PDF'))
-    else:
-        raise Http404(_('Bad method'))
-
-
 class CustomPDFFormView(AjaxableFormView):
     form_class = forms.CustomPDFForm
     title = ugettext_lazy('Download custom PDF')
     submit = ugettext_lazy('Download')
+    honeypot = True
 
-    def __call__(self, request):
-        from copy import copy
-        if request.method == 'POST':
-            request.GET = copy(request.GET)
-            request.GET['next'] = "%s?%s" % (reverse('catalogue.views.download_custom_pdf', args=[request.GET.get('slug')]),
-                                             request.POST.urlencode())
-        return super(CustomPDFFormView, self).__call__(request)
+    def form_args(self, request, obj):
+        """Override to parse view args and give additional args to the form."""
+        return (obj,), {}
 
-    def get_object(self, request):
-        return get_object_or_404(models.Book, slug=request.GET.get('slug'))
+    def get_object(self, request, slug, *args, **kwargs):
+        return get_object_or_404(models.Book, slug=slug)
 
     def context_description(self, request, obj):
         return obj.pretty_title()
-
-    def success(self, *args):
-        pass
index 1d2fbba..6238ccb 100644 (file)
@@ -3,7 +3,7 @@
 # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
 #
 from django.db import models
-
+from celery.task import task
 from sortify import sortify
 
 from catalogue.models import Book
@@ -19,15 +19,17 @@ class Note(models.Model):
         ordering = ['sort_key']
 
 
-def notes_from_book(sender, **kwargs):
-    from librarian import html
-
-    Note.objects.filter(book=sender).delete()
-    if sender.html_file:
-        for anchor, text_str, html_str in html.extract_annotations(sender.html_file.path):
-            Note.objects.create(book=sender, anchor=anchor,
+@task(ignore_result=True)
+def build_notes(book_id):
+    book = Book.objects.get(pk=book_id)
+    Note.objects.filter(book=book).delete()
+    if book.html_file:
+        from librarian import html
+        for anchor, text_str, html_str in html.extract_annotations(book.html_file.path):
+            Note.objects.create(book=book, anchor=anchor,
                                html=html_str, 
                                sort_key=sortify(text_str).strip()[:128])
-
-# always re-extract notes after making a HTML in a Book
-Book.html_built.connect(notes_from_book)
+    
+@Book.html_built.connect
+def notes_from_book(sender, **kwargs):
+    build_notes.delat(sender)
index 91fb35f..202acdd 100644 (file)
@@ -1,16 +1,10 @@
 # -*- coding: utf-8 -*-
 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 os import path
+from django.test import TestCase
 from picture.models import Picture
 
-from nose.tools import raises
-import tempfile
-from os import unlink, path, makedirs
-
 
 class PictureTest(TestCase):
     
index 2ea1a86..c7282fd 100755 (executable)
@@ -3,11 +3,13 @@
 
 <form action="{% url social_unlike_book view_kwargs.slug %}" method="post" accept-charset="utf-8"
        class="cuteform{% if placeholdize %} hidelabels{% endif %}">
+{% csrf_token %}
     <input type="submit" value="{% trans "Remove from my shelf" %}"/>
 </form>
 
 <form action="{{ request.get_full_path }}" method="post" accept-charset="utf-8"
        class="cuteform{% if placeholdize %} hidelabels{% endif %}">
+{% csrf_token %}
 <ol>
     <div id="id___all__"></div>
     {{ form.as_ul }}
index 3e71000..ea1d926 100755 (executable)
@@ -1,8 +1,11 @@
 {% load i18n %}
+{% load honeypot %}
+
 <h1>{% trans "Didn't find a book? Make a suggestion." %}</h1>
 
 <form id='suggest-publishing-form' action="{% url suggest_publishing %}" method="post" accept-charset="utf-8" class="cuteform">
 {% csrf_token %}
+{% render_honeypot_field %}
 <ol>
     <li><span class="error">{{ form.contact.errors }}</span><label for="id_contact">{{ form.contact.label }}</label> {{ form.contact }}</li>
 
index 15b65f2..035074d 100644 (file)
@@ -6,7 +6,6 @@ from django.utils.translation import ugettext_lazy as _
 
 from ajaxable.utils import AjaxableFormView
 from suggest import forms
-from suggest.models import Suggestion, PublishingSuggestion
 
 
 class PublishingSuggestionFormView(AjaxableFormView):
@@ -14,6 +13,7 @@ class PublishingSuggestionFormView(AjaxableFormView):
     title = _('Report a bug or suggestion')
     template = "publishing_suggest.html"
     success_message = _('Report was sent successfully.')
+    honeypot = True
 
 
 class SuggestionFormView(AjaxableFormView):
@@ -21,3 +21,4 @@ class SuggestionFormView(AjaxableFormView):
     title = _('Report a bug or suggestion')
     submit = _('Send report')
     success_message = _('Report was sent successfully.')
+    honeypot = True
diff --git a/apps/waiter/__init__.py b/apps/waiter/__init__.py
new file mode 100644 (file)
index 0000000..d3696b7
--- /dev/null
@@ -0,0 +1,8 @@
+"""
+Celery waiter.
+
+Takes orders for files generated by async Celery tasks.
+Serves the file when ready. Kindly asks the user to wait if not.
+
+Author: Radek Czajka <radoslaw.czajka@nowoczesnapolska.org.pl>
+"""
\ No newline at end of file
diff --git a/apps/waiter/locale/pl/LC_MESSAGES/django.mo b/apps/waiter/locale/pl/LC_MESSAGES/django.mo
new file mode 100644 (file)
index 0000000..8f8afb7
Binary files /dev/null and b/apps/waiter/locale/pl/LC_MESSAGES/django.mo differ
diff --git a/apps/waiter/locale/pl/LC_MESSAGES/django.po b/apps/waiter/locale/pl/LC_MESSAGES/django.po
new file mode 100644 (file)
index 0000000..bbc9f8d
--- /dev/null
@@ -0,0 +1,67 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2012-03-22 15:54+0100\n"
+"PO-Revision-Date: 2012-03-22 15:54+0100\n"
+"Last-Translator: Radek Czajka <radoslaw.czajka@nowoczesnapolska.org.pl>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2)\n"
+
+#: templates/waiter/wait.html:7
+#: templates/waiter/wait.html.py:33
+msgid "The file is ready for download!"
+msgstr "Plik jest gotowy do pobrania!"
+
+#: templates/waiter/wait.html:10
+#: templates/waiter/wait.html.py:42
+msgid "Your file is being prepared, please wait."
+msgstr "Plik jest generowany, proszę czekać."
+
+#: templates/waiter/wait.html:12
+#: templates/waiter/wait.html.py:51
+msgid "Something went wrong."
+msgstr "Coś poszło nie tak."
+
+#: templates/waiter/wait.html:36
+#, python-format
+msgid ""
+"Your file is ready!\n"
+"        If the download doesn't start in a few seconds,\n"
+"        feel free to use this <a href=\"%(file_url)s\">direct link</a>."
+msgstr ""
+"Twój plik jest gotowy!\n"
+"Jeśli pobieranie nie zacznie się w ciągu kilku sekund,\n"
+"skorzystaj z tego <a href=\"%(file_url)s\">bezpośredniego linku</a>."
+
+#: templates/waiter/wait.html:45
+#, python-format
+msgid "The file you requested was: <em>%(d)s</em>."
+msgstr "Zamówiony plik to: <em>%(d)s</em>."
+
+#: templates/waiter/wait.html:47
+msgid ""
+"<strong>Be aware:</strong> Generating the file can take a while.\n"
+"        Please be patient, or bookmark this page and come back later.</p>"
+msgstr ""
+"<strong>Uwaga:</strong> Generowanie pliku może trwać dłuższą chwilę.\n"
+"Poczekaj cierpliwie, albo dodaj tę stronę do zakładek i wróć później.</p>"
+
+#: templates/waiter/wait.html:55
+#, python-format
+msgid ""
+"Something seems to have gone wrong while generating your file.\n"
+"        Please order it again or <a id='suggest' class='ajaxable' href=\"%(s)s\">complain to us</a> about it."
+msgstr ""
+"Wygląda na to, że coś poszło źle podczas generowania Twojego pliku.\n"
+"Spróbuj zamówić go jeszcze raz albo  <a id='suggest' class='ajaxable' href=\"%(s)s\">napisz do nas</a>."
+
diff --git a/apps/waiter/migrations/0001_initial.py b/apps/waiter/migrations/0001_initial.py
new file mode 100644 (file)
index 0000000..a75884b
--- /dev/null
@@ -0,0 +1,39 @@
+# 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 model 'WaitedFile'
+        db.create_table('waiter_waitedfile', (
+            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+            ('path', self.gf('django.db.models.fields.CharField')(unique=True, max_length=255, db_index=True)),
+            ('task_id', self.gf('django.db.models.fields.CharField')(db_index=True, max_length=128, null=True, blank=True)),
+            ('task', self.gf('picklefield.fields.PickledObjectField')(null=True)),
+            ('description', self.gf('django.db.models.fields.CharField')(max_length=255, null=True, blank=True)),
+        ))
+        db.send_create_signal('waiter', ['WaitedFile'])
+
+
+    def backwards(self, orm):
+        
+        # Deleting model 'WaitedFile'
+        db.delete_table('waiter_waitedfile')
+
+
+    models = {
+        'waiter.waitedfile': {
+            'Meta': {'object_name': 'WaitedFile'},
+            'description': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'path': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255', 'db_index': 'True'}),
+            'task': ('picklefield.fields.PickledObjectField', [], {'null': 'True'}),
+            'task_id': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '128', 'null': 'True', 'blank': 'True'})
+        }
+    }
+
+    complete_apps = ['waiter']
diff --git a/apps/waiter/migrations/__init__.py b/apps/waiter/migrations/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/apps/waiter/models.py b/apps/waiter/models.py
new file mode 100644 (file)
index 0000000..10f9289
--- /dev/null
@@ -0,0 +1,65 @@
+from os.path import join, isfile
+from django.core.urlresolvers import reverse
+from django.db import models
+from waiter.settings import WAITER_URL, WAITER_MAX_QUEUE
+from waiter.utils import check_abspath
+from picklefield import PickledObjectField
+
+
+class WaitedFile(models.Model):
+    path = models.CharField(max_length=255, unique=True, db_index=True)
+    task_id = models.CharField(max_length=128, db_index=True, null=True, blank=True)
+    task = PickledObjectField(null=True, editable=False)
+    description = models.CharField(max_length=255, null=True, blank=True)
+
+    @classmethod
+    def exists(cls, path):
+        """Returns opened file or None.
+        
+        `path` is relative to WAITER_ROOT.
+        Won't open a path leading outside of WAITER_ROOT.
+        """
+        abs_path = check_abspath(path)
+        # Pre-fetch objects for deletion to avoid minor race condition
+        relevant = [o.id for o in cls.objects.filter(path=path)]
+        if isfile(abs_path):
+            cls.objects.filter(id__in=relevant).delete()
+            return True
+        else:
+            return False
+
+    @classmethod
+    def can_order(cls, path):
+        return (cls.objects.filter(path=path).exists() or
+                cls.exists(path) or
+                cls.objects.count() < WAITER_MAX_QUEUE
+                )
+
+    def is_stale(self):
+        if self.task is None:
+            # Race; just let the other task roll. 
+            return False
+        if self.task.status not in (u'PENDING', u'STARTED', u'SUCCESS', u'RETRY'):
+            return True
+        return False
+
+    @classmethod
+    def order(cls, path, task_creator, description=None):
+        """
+        Returns an URL for the user to follow.
+        If the file is ready, returns download URL.
+        If not, starts preparing it and returns waiting URL.
+
+        task_creator: function taking a path and generating the file;
+        description: a string or string proxy with a description for user;
+        """
+        already = cls.exists(path)
+        if not already:
+            waited, created = cls.objects.get_or_create(path=path)
+            if created or waited.is_stale():
+                waited.task = task_creator(check_abspath(path))
+                waited.task_id = waited.task.task_id
+                waited.description = description
+                waited.save()
+            return reverse("waiter", args=[path])
+        return join(WAITER_URL, path)
diff --git a/apps/waiter/settings.py b/apps/waiter/settings.py
new file mode 100644 (file)
index 0000000..fef4a7e
--- /dev/null
@@ -0,0 +1,18 @@
+from os.path import join
+from django.conf import settings
+
+try:
+    WAITER_ROOT = settings.WAITER_ROOT
+except AttributeError:
+    WAITER_ROOT = join(settings.MEDIA_ROOT, 'waiter')
+
+try:
+    WAITER_URL = settings.WAITER_URL
+except AttributeError:
+    WAITER_URL = join(settings.MEDIA_URL, 'waiter')
+
+try:
+    WAITER_MAX_QUEUE = settings.WAITER_MAX_QUEUE
+except AttributeError:
+    WAITER_MAX_QUEUE = 20
+
diff --git a/apps/waiter/tasks.py b/apps/waiter/tasks.py
new file mode 100644 (file)
index 0000000..4c3933e
--- /dev/null
@@ -0,0 +1,7 @@
+from celery.signals import task_postrun
+from waiter.models import WaitedFile
+
+
+def task_delete_after(task_id=None, **kwargs):
+    WaitedFile.objects.filter(task_id=task_id).delete()
+task_postrun.connect(task_delete_after)
diff --git a/apps/waiter/templates/waiter/wait.html b/apps/waiter/templates/waiter/wait.html
new file mode 100644 (file)
index 0000000..e15bd64
--- /dev/null
@@ -0,0 +1,93 @@
+{% extends "base.html" %}
+{% load i18n %}
+{% load url from future %}
+
+{% block titleextra %}
+{% if file_url %}
+    {% trans "The file is ready for download!" %}
+{% else %}
+    {% if waiting %}
+        {% trans "Your file is being prepared, please wait." %}
+    {% else %}
+        {% trans "Something went wrong." %}
+    {% endif %}
+{% endif %}
+{% endblock %}
+
+
+{% block extrahead %}
+{% if file_url %}
+    <meta http-equiv="refresh" content="3; url={{ file_url }}" />
+{% else %}
+    {% if waiting %}
+        <noscript>
+            <meta http-equiv="refresh" content="10" />
+        </noscript>
+    {% endif %}
+{% endif %}
+{% endblock %}
+
+
+{% block body %}
+{% if file_url %}
+    <h1>{% trans "The file is ready for download!" %}</h1>
+
+    <div class="normal-text white-box">
+    <p>{% blocktrans %}Your file is ready!
+        If the download doesn't start in a few seconds,
+        feel free to use this <a href="{{ file_url }}">direct link</a>.{% endblocktrans %}</p>
+    </div>
+{% else %}
+ {% if waiting %}
+    <h1><img src="{{ STATIC_URL }}img/indicator.gif" alt="{% trans 'Please wait' %}"/>
+        {% trans "Your file is being prepared, please wait." %}</h1>
+
+    <div class="normal-text">
+    <p>{% blocktrans with d=waiting.description %}The file you requested was: <em>{{d}}</em>.{% endblocktrans %}</p>
+
+    <p>{% blocktrans %}<strong>Be aware:</strong> Generating the file can take a while.
+        Please be patient, or bookmark this page and come back later.</p>{% endblocktrans %}
+    </div>
+ {% else %}
+    <h1>{% trans "Something went wrong." %}</h1>
+
+    <div class="normal-text">
+    {% url 'suggest' as s %}
+    <p>{% blocktrans %}Something seems to have gone wrong while generating your file.
+        Please order it again or <a id='suggest' class='ajaxable' href="{{s}}">complain to us</a> about it.{% endblocktrans %}</p>
+    </div>
+ {% endif %}
+{% endif %}
+       
+{% endblock %}
+
+{% block extrabody %}
+{% if waiting %}
+<script language="JavaScript">
+<!--
+(function($) {
+    $(function(){
+
+function wait() {
+    $.ajax({
+        href: '',
+        success: function(data) {
+            if (data) {
+                location.reload();
+            }
+            else
+                setTimeout(wait, 10*1000);
+        },
+        error: function(xhr) {
+            location.reload();
+        }
+    });
+}
+setTimeout(wait, 10*1000);
+
+    });
+})(jQuery);
+//-->
+</script>
+{% endif %}
+{% endblock %}
diff --git a/apps/waiter/urls.py b/apps/waiter/urls.py
new file mode 100644 (file)
index 0000000..484ef3e
--- /dev/null
@@ -0,0 +1,5 @@
+from django.conf.urls.defaults import *
+
+urlpatterns = patterns('waiter.views',
+    url(r'^(?P<path>.*)$', 'wait', name='waiter'),
+)
diff --git a/apps/waiter/utils.py b/apps/waiter/utils.py
new file mode 100644 (file)
index 0000000..0957e9d
--- /dev/null
@@ -0,0 +1,17 @@
+from os.path import abspath, join, exists
+from shutil import rmtree
+from waiter.settings import WAITER_ROOT
+
+
+def check_abspath(path):
+    abs_path = abspath(join(WAITER_ROOT, path))
+    if not abs_path.startswith(WAITER_ROOT):
+        raise ValueError('Path not inside WAITER_ROOT.')
+    return abs_path
+
+
+def clear_cache(path):
+    abs_path = check_abspath(path)
+    if exists(abs_path):
+        rmtree(abs_path)
+    
diff --git a/apps/waiter/views.py b/apps/waiter/views.py
new file mode 100644 (file)
index 0000000..e38bd8f
--- /dev/null
@@ -0,0 +1,19 @@
+from os.path import join
+from waiter.models import WaitedFile
+from waiter.settings import WAITER_URL
+from django.shortcuts import render, get_object_or_404
+from django.http import HttpResponse
+
+def wait(request, path):
+    if WaitedFile.exists(path):
+        file_url = join(WAITER_URL, path)
+    else:
+        file_url = ""
+        waiting = get_object_or_404(WaitedFile, path=path)
+        if waiting.is_stale():
+            waiting = None
+
+    if request.is_ajax():
+        return HttpResponse(file_url)
+    else:
+        return render(request, "waiter/wait.html", locals())
index 23cfb59..ff381b5 100644 (file)
@@ -5,7 +5,18 @@
 
 {% block branding %}
 <h1 id="site-name">{% trans "Site administration" %} - WolneLektury.pl</h1>
-<p style="float: right; font-size: 11px; padding-right: 10px;"><a href="/rosetta/">{% trans "Translations" %}</a></p>
+<p style="font-size: 11px;margin-left:10px;">
+    <!--a href="/rosetta/">{% trans "Translations" %}</a-->
+    <a href="/admin/chunks/chunk/promo/">promobox</a> |
+    <a href="/admin/social/cite">cytaty</a> |
+    <a href="/admin/infopages/infopage/">info</a> |
+    <a href="/admin/catalogue/book/">książki</a> |
+    <a href="/admin/catalogue/tag/">tagi</a> |
+    <a href="/admin/catalogue/collection/">kolekcje</a> |
+    <a href="/admin/pdcounter/">licznik domeny</a> |
+    <a href="/admin/sponsors/">sponsorzy</a> |
+    <a href="/admin/suggest/suggestion/">sugestie</a>
+</p>
 {% endblock %}
 
 {% block nav-global %}{% endblock %}
\ No newline at end of file
index 09e567a..0ec95b2 100644 (file)
@@ -3,6 +3,7 @@
 
 {% block content %}
     <form action="{% url import_book %}" method="post" enctype="multipart/form-data">
+    {% csrf_token %}
         <p><input type="file" id="id_book_xml_file" name="book_xml_file" /> <input type="submit" value="{% trans "Import book" %}"/></p>
     </form>
     {{ block.super }}
index 7fbd570..6d71cd5 100644 (file)
@@ -3,6 +3,7 @@
 
 <form action="{{ request.get_full_path }}" method="post" accept-charset="utf-8"
        class="cuteform{% if placeholdize %} hidelabels{% endif %}">
+{% csrf_token %}
 <ol>
     <div id="id_{% if form_prefix %}{{ form_prefix }}-{% endif %}__all__"></div>
     {{ form.as_ul }}
index f879d46..689c1ba 100755 (executable)
@@ -1,5 +1,6 @@
 {% extends "auth/login.html" %}
 {% load i18n %}
+{% load honeypot %}
 
 {% block extra %}
 
@@ -9,6 +10,8 @@
 
 <form action="{% url register %}" method="post" accept-charset="utf-8"
        class="cuteform hidelabels">
+{% csrf_token %}
+{% honeypot_render_field %}
 <ol>
     <div id="id_register-__all__"></div>
     {{ register_form.as_ul }}
index 4e45207..0fd06f8 100755 (executable)
@@ -10,6 +10,7 @@
     <p>{% blocktrans %}Confirm to authorize access to Wolne Lektury as user <strong>{{ user}}</strong>.{% endblocktrans %}</p>
 
     <form action="{% url piston.authentication.oauth_user_auth %}" method="POST">
+    {% csrf_token %}
       {{ form.as_p }}
       <button type="submit">Confirm</button>
     </form>
index a4a7830..f67d9f3 100644 (file)
                 <div id="lang-menu-items">
                 {% for lang in LANGUAGES %}
                     <form action="{% url django.views.i18n.set_language %}" method="post">
+                    {% csrf_token %}
                     <input type="hidden" name="language" value="{{ lang.0 }}" />
                     <button type="submit"
                         class="{% ifequal lang.0 LANGUAGE_CODE %}active{% endifequal %} mono"
index 37df778..f8de19e 100644 (file)
@@ -53,6 +53,7 @@ class RegisterFormView(AjaxableFormView):
     submit = _('Register')
     ajax_redirect = True
     form_prefix = 'register'
+    honeypot = True
 
     def __call__(self, request):
         if request.user.is_authenticated():
index 9e13b0c..b8e34e0 160000 (submodule)
@@ -1 +1 @@
-Subproject commit 9e13b0c994e9d481008bef7006a74609adfd16f8
+Subproject commit b8e34e0e6730ef76a353a15ff653faa9e8c88a77
index bf8bb43..3c85e31 100644 (file)
@@ -8,7 +8,9 @@ django-rosetta>=0.5.3
 django-maintenancemode>=0.9
 django-piston
 django-jsonfield
+django-picklefield
 django-allauth
+django-honeypot
 
 python-memcached
 piwik
index e00d269..679abec 100644 (file)
@@ -6,6 +6,7 @@ from settings.basic import *
 from settings.auth import *
 from settings.cache import *
 from settings.celery import *
+from settings.contrib import *
 from settings.custom import *
 from settings.locale import *
 from settings.static import *
@@ -27,6 +28,7 @@ MIDDLEWARE_CLASSES = [
     'django.middleware.cache.UpdateCacheMiddleware',
     'django.middleware.common.CommonMiddleware',
     'django.contrib.sessions.middleware.SessionMiddleware',
+    'django.middleware.csrf.CsrfViewMiddleware',
     'django.contrib.auth.middleware.AuthenticationMiddleware',
     'django.middleware.doc.XViewMiddleware',
     'pagination.middleware.PaginationMiddleware',
@@ -40,8 +42,32 @@ MIDDLEWARE_CLASSES = [
 
 ROOT_URLCONF = 'wolnelektury.urls'
 
-INSTALLED_APPS = [
+# These are the ones we should test.
+INSTALLED_APPS_OUR = [
     'wolnelektury_core',
+    # our
+    'ajaxable',
+    'api',
+    'catalogue',
+    'chunks',
+    'dictionary',
+    'infopages',
+    'lesmianator',
+    #'lessons',
+    'newtagging',
+    'opds',
+    'pdcounter',
+    'reporting',
+    'sponsors',
+    'stats',
+    'suggest',
+    'picture',
+    'search',
+    'social',
+    'waiter',
+    ]
+
+INSTALLED_APPS_CONTRIB = [
     # external
     'django.contrib.auth',
     'django.contrib.contenttypes',
@@ -57,6 +83,7 @@ INSTALLED_APPS = [
     'sorl.thumbnail',
     'djcelery',
     'djkombu',
+    'honeypot',
     #    'django_nose',
 
     #allauth stuff
@@ -72,27 +99,9 @@ INSTALLED_APPS = [
     # included
     'compress',
     'modeltranslation',
+    ]
 
-    # our
-    'ajaxable',
-    'api',
-    'catalogue',
-    'chunks',
-    'dictionary',
-    'infopages',
-    'lesmianator',
-    #'lessons',
-    'newtagging',
-    'opds',
-    'pdcounter',
-    'reporting',
-    'sponsors',
-    'stats',
-    'suggest',
-    'picture',
-    'search',
-    'social',
-]
+INSTALLED_APPS = INSTALLED_APPS_OUR + INSTALLED_APPS_CONTRIB
 
 # Load localsettings, if they exist
 try:
index cb5dde9..ad37707 100644 (file)
@@ -9,3 +9,4 @@ BROKER_PASSWORD = "guest"
 BROKER_VHOST = "/"
 
 CELERY_EAGER_PROPAGATES_EXCEPTIONS = True
+CELERY_SEND_TASK_ERROR_EMAILS = True
diff --git a/wolnelektury/settings/contrib.py b/wolnelektury/settings/contrib.py
new file mode 100644 (file)
index 0000000..ba6603b
--- /dev/null
@@ -0,0 +1 @@
+HONEYPOT_FIELD_NAME = 'miut'
index 3031f4e..9abb5ed 100644 (file)
@@ -21,3 +21,13 @@ ALL_MOBI_ZIP = 'wolnelektury_pl_mobi'
 
 CATALOGUE_DEFAULT_LANGUAGE = 'pol'
 PUBLISH_PLAN_FEED = 'http://redakcja.wolnelektury.pl/documents/track/editor-proofreading/?published=false'
+
+# limit rate for ebooks creation
+CATALOGUE_PDF_RATE_LIMIT = '1/m'
+CATALOGUE_EPUB_RATE_LIMIT = '6/m'
+CATALOGUE_MOBI_RATE_LIMIT = '5/m'
+CATALOGUE_CUSTOMPDF_RATE_LIMIT = '1/m'
+
+# set to 'new' or 'old' to skip time-consuming test
+# for TeX morefloats library version
+LIBRARIAN_PDF_MOREFLOATS = None
index ce51801..5323ada 100644 (file)
@@ -34,6 +34,7 @@ urlpatterns += patterns('',
     url(r'^info/', include('infopages.urls')),
     url(r'^ludzie/', include('social.urls')),
     url(r'^uzytkownik/', include('allauth.urls')),
+    url(r'^czekaj/', include('waiter.urls')),
 
     # Admin panel
     url(r'^admin/catalogue/book/import$', 'catalogue.views.import_book', name='import_book'),