celery waiter for custompdf
authorRadek Czajka <radoslaw.czajka@nowoczesnapolska.org.pl>
Thu, 22 Mar 2012 14:56:18 +0000 (15:56 +0100)
committerRadek Czajka <radoslaw.czajka@nowoczesnapolska.org.pl>
Thu, 22 Mar 2012 14:56:18 +0000 (15:56 +0100)
12 files changed:
apps/catalogue/forms.py
apps/catalogue/utils.py
apps/catalogue/views.py
apps/waiter/__init__.py
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/models.py
apps/waiter/templates/waiter/wait.html
apps/waiter/views.py
lib/librarian
wolnelektury/settings/celery.py
wolnelektury/settings/custom.py

index 75d9ab9..d191d46 100644 (file)
@@ -35,54 +35,46 @@ class DownloadFormatsForm(forms.Form):
             widget=forms.CheckboxSelectMultiple)
 
     def __init__(self, *args, **kwargs):
             widget=forms.CheckboxSelectMultiple)
 
     def __init__(self, *args, **kwargs):
-         super(DownloadFormatsForm, self).__init__(*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')),
+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):
     )
 
 
 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, *args, **kwargs):
+        super(CustomPDFForm, self).__init__(*args, **kwargs)
+        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)
 
     @property
     def customizations(self):
         c = []
 
     @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()
         c.sort()
-
         return c
         return c
-
index 185f5fa..949ac96 100644 (file)
@@ -140,7 +140,7 @@ class AttachmentHttpResponse(HttpResponse):
             for chunk in read_chunks(f):
                 self.write(chunk)
 
             for chunk in read_chunks(f):
                 self.write(chunk)
 
-@task
+@task(rate_limit=settings.CATALOGUE_CUSTOMPDF_RATE_LIMIT)
 def async_build_pdf(book_id, customizations, file_name):
     """
     A celery task to generate pdf files.
 def async_build_pdf(book_id, customizations, file_name):
     """
     A celery task to generate pdf files.
index c8549c2..0c05d17 100644 (file)
@@ -22,7 +22,7 @@ from ajaxable.utils import JSONResponse, AjaxableFormView
 
 from catalogue import models
 from catalogue import forms
 
 from catalogue import models
 from catalogue import forms
-from catalogue.utils import (split_tags, AttachmentHttpResponse,
+from catalogue.utils import (split_tags,
     async_build_pdf, MultiQuerySet)
 from pdcounter import models as pdcounter_models
 from pdcounter import views as pdcounter_views
     async_build_pdf, MultiQuerySet)
 from pdcounter import models as pdcounter_models
 from pdcounter import views as pdcounter_views
@@ -543,7 +543,7 @@ def download_custom_pdf(request, slug, method='GET'):
 
             url = WaitedFile.order(pdf_file,
                     lambda p: async_build_pdf.delay(book.id, cust, p),
 
             url = WaitedFile.order(pdf_file,
                     lambda p: async_build_pdf.delay(book.id, cust, p),
-                    "%s: %s" % (book.pretty_title(), ", ".join(cust))
+                    book.pretty_title()
                 )
             return redirect(url)
         else:
                 )
             return redirect(url)
         else:
index 5aabbd6..d3696b7 100644 (file)
@@ -1,5 +1,8 @@
 """
 """
-Waiting for and serving files generated in Celery to the user.
+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
 
 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>."
+
index fd97301..26a9a6d 100644 (file)
@@ -1,7 +1,9 @@
 from os.path import join, abspath, exists
 from os.path import join, abspath, exists
+from django.core.urlresolvers import reverse
 from django.db import models
 from waiter.settings import WAITER_ROOT, WAITER_URL
 from django.db import models
 from waiter.settings import WAITER_ROOT, WAITER_URL
-from django.core.urlresolvers import reverse
+from djcelery.models import TaskMeta
+
 
 class WaitedFile(models.Model):
     path = models.CharField(max_length=255, unique=True, db_index=True)
 
 class WaitedFile(models.Model):
     path = models.CharField(max_length=255, unique=True, db_index=True)
@@ -23,31 +25,44 @@ class WaitedFile(models.Model):
         Won't open a path leading outside of WAITER_ROOT.
         """
         abs_path = cls.abspath(path)
         Won't open a path leading outside of WAITER_ROOT.
         """
         abs_path = cls.abspath(path)
-        # Pre-fetch objects to avoid minor race condition
+        # Pre-fetch objects for deletion to avoid minor race condition
         relevant = [o.id for o in cls.objects.filter(path=path)]
         relevant = [o.id for o in cls.objects.filter(path=path)]
-        print abs_path
         if exists(abs_path):
             cls.objects.filter(id__in=relevant).delete()
             return True
         else:
             return False
 
         if exists(abs_path):
             cls.objects.filter(id__in=relevant).delete()
             return True
         else:
             return False
 
+    def is_stale(self):
+        if self.task is None:
+            # Race; just let the other task roll. 
+            return False
+        try:
+            meta = TaskMeta.objects.get(task_id=self.task)
+            assert meta.status in (u'PENDING', u'STARTED', u'SUCCESS', u'RETRY')
+        except TaskMeta.DoesNotExist:
+            # Might happen it's not yet there.
+            pass
+        except AssertionError:
+            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.
     @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)
         """
         already = cls.exists(path)
         if not already:
             waited, created = cls.objects.get_or_create(path=path)
-            if created:
-                # TODO: makedirs
+            if created or waited.is_stale():
                 waited.task = task_creator(cls.abspath(path))
                 waited.task = task_creator(cls.abspath(path))
-                print waited.task
                 waited.description = description
                 waited.save()
                 waited.description = description
                 waited.save()
-            # TODO: it the task exists, if stale delete, send some mail and restart
             return reverse("waiter", args=[path])
         return join(WAITER_URL, path)
             return reverse("waiter", args=[path])
         return join(WAITER_URL, path)
index 7c5a884..f4dedc7 100644 (file)
@@ -1,21 +1,92 @@
 {% extends "base.html" %}
 {% extends "base.html" %}
+{% load i18n %}
+{% load url from future %}
 
 
-{% block titleextra %}Prosię ciekać{% endblock %}
+{% 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 %}
 
 
 {% block extrahead %}
-       {# TODO: Not like that! What are you, caveman?! #}
-       <meta http-equiv="refresh" content="{% if file_url %}0; url={{ file_url }}{% else %}5{% endif %}" />
+{% 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 %}
 {% endblock %}
 
 
 {% block body %}
-       {% if file_url %}
-       <p>Plik jest gotowy! Jeśli pobieranie nie zacznie się automatycznie,
-               oto <a href="{{ file_url }}">bezpośredni link</a>.</p>
-       {% else %}
-               <p>Idź na kawę. To trochę potrwa.</p>
-
-               <p>{{ waiting_for.description }}</p>
-       {% endif %}
+{% 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>{% 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() {
+            setTimeout(wait, 10*1000);
+        }
+    });
+}
+setTimeout(wait, 10*1000);
+
+    });
+})(jQuery);
+//-->
+</script>
+{% endif %}
 {% endblock %}
 {% endblock %}
index 81cd3b4..e38bd8f 100644 (file)
@@ -1,12 +1,19 @@
 from os.path import join
 from waiter.models import WaitedFile
 from waiter.settings import WAITER_URL
 from os.path import join
 from waiter.models import WaitedFile
 from waiter.settings import WAITER_URL
-from django.shortcuts import get_object_or_404, render, redirect
+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:
 
 def wait(request, path):
     if WaitedFile.exists(path):
         file_url = join(WAITER_URL, path)
     else:
-        waiting_for = get_object_or_404(WaitedFile, path=path)
-        # TODO: check if not stale, inform the user and send some mail if so.
-    return render(request, "waiter/wait.html", locals())
+        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 9e13b0c..b8e34e0 160000 (submodule)
@@ -1 +1 @@
-Subproject commit 9e13b0c994e9d481008bef7006a74609adfd16f8
+Subproject commit b8e34e0e6730ef76a353a15ff653faa9e8c88a77
index cb5dde9..ad37707 100644 (file)
@@ -9,3 +9,4 @@ BROKER_PASSWORD = "guest"
 BROKER_VHOST = "/"
 
 CELERY_EAGER_PROPAGATES_EXCEPTIONS = True
 BROKER_VHOST = "/"
 
 CELERY_EAGER_PROPAGATES_EXCEPTIONS = True
+CELERY_SEND_TASK_ERROR_EMAILS = True
index 3031f4e..97d9588 100644 (file)
@@ -21,3 +21,5 @@ ALL_MOBI_ZIP = 'wolnelektury_pl_mobi'
 
 CATALOGUE_DEFAULT_LANGUAGE = 'pol'
 PUBLISH_PLAN_FEED = 'http://redakcja.wolnelektury.pl/documents/track/editor-proofreading/?published=false'
 
 CATALOGUE_DEFAULT_LANGUAGE = 'pol'
 PUBLISH_PLAN_FEED = 'http://redakcja.wolnelektury.pl/documents/track/editor-proofreading/?published=false'
+
+CATALOGUE_CUSTOMPDF_RATE_LIMIT = '6/m'