From aae5369e6dab510705fdfa1265cf9e095d24a97d Mon Sep 17 00:00:00 2001 From: Radek Czajka Date: Thu, 22 Mar 2012 15:56:18 +0100 Subject: [PATCH] celery waiter for custompdf --- apps/catalogue/forms.py | 68 +++++++------- apps/catalogue/utils.py | 2 +- apps/catalogue/views.py | 4 +- apps/waiter/__init__.py | 5 +- apps/waiter/locale/pl/LC_MESSAGES/django.mo | Bin 0 -> 1858 bytes apps/waiter/locale/pl/LC_MESSAGES/django.po | 67 ++++++++++++++ apps/waiter/models.py | 29 ++++-- apps/waiter/templates/waiter/wait.html | 93 +++++++++++++++++--- apps/waiter/views.py | 15 +++- lib/librarian | 2 +- wolnelektury/settings/celery.py | 1 + wolnelektury/settings/custom.py | 2 + 12 files changed, 223 insertions(+), 65 deletions(-) create mode 100644 apps/waiter/locale/pl/LC_MESSAGES/django.mo create mode 100644 apps/waiter/locale/pl/LC_MESSAGES/django.po diff --git a/apps/catalogue/forms.py b/apps/catalogue/forms.py index 75d9ab997..d191d46db 100644 --- a/apps/catalogue/forms.py +++ b/apps/catalogue/forms.py @@ -35,54 +35,46 @@ class DownloadFormatsForm(forms.Form): 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): - 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 = [] - 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 - diff --git a/apps/catalogue/utils.py b/apps/catalogue/utils.py index 185f5fa34..949ac96b2 100644 --- a/apps/catalogue/utils.py +++ b/apps/catalogue/utils.py @@ -140,7 +140,7 @@ class AttachmentHttpResponse(HttpResponse): 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. diff --git a/apps/catalogue/views.py b/apps/catalogue/views.py index c8549c26f..0c05d17a8 100644 --- a/apps/catalogue/views.py +++ b/apps/catalogue/views.py @@ -22,7 +22,7 @@ from ajaxable.utils import JSONResponse, AjaxableFormView 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 @@ -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), - "%s: %s" % (book.pretty_title(), ", ".join(cust)) + book.pretty_title() ) return redirect(url) else: diff --git a/apps/waiter/__init__.py b/apps/waiter/__init__.py index 5aabbd62d..d3696b741 100644 --- a/apps/waiter/__init__.py +++ b/apps/waiter/__init__.py @@ -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 """ \ 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 index 0000000000000000000000000000000000000000..8f8afb7de4215c3bd40da47af73d10f49d5e88b6 GIT binary patch literal 1858 zcmb7EO>Y}T7+#=!$lQ>61M#M)O`>KucG^mnYn!$qtpaHqrEU=j3BBGOZ^pZxS!Q;- zwdtu+R0&QTI8@~qATB7ksuvVnf#8|>e(f5h&;;&~^5e*)hJo_y8w&H^t0mw^oY4)`tb7s$N#n&&;i z|36N6-p|0VU$6E3H*kml>%b3y3_Jte13E%K0io^zss%G1OjPSl{5rlc<-tw8IyN_2 z#l5S7xMU;Roz>?jbX_Q6IF~A>d{@v=rh-CNlyd@MDtB=ZH0b%-P6b;*13?*dgvwV) z8yaYxjM*fxAT4fUXsU=pJ%%WU38kD16LeOyo_8CE5Q94_#Mn}ou5{6_>9J5r8ONU8D-9=l(v9S3fg(+LxUE~qp=RqnKY1fEh_tfbn zyKGPOKru_5CTJA4v&uc@cM^YLa0V?G@&8fM!Hm~=4j#Q9;_ko zRRJ0zqLOW!I%3>Ay%a6O6TSlPv%|m$#sRgxXxcp7Ba=Y-ND4WBo zbcYncMOd|%lg-&65`{7jMTvZeT19r{h2Btzl!iu#I(mRB9};+&BG(LG7AAF>ilh-? zPN`IhOH<%&8$H5_zZv;=g|SkrHf>*7|K!T`4Z6Frb9?jF&Bl(%w8{M&HkOfpwTP|X z*KKNS-|GA82qK4){}FnlO>528n%_L_4uf68sK&HcV% zWzlKcv}uPUkwHdyOqtae?;r`+~=9 z+PZS{`W+WfXKU+nDxKJo4C1)4d1K=!_*T$ttZS8{SN#4YLr}T6pRZQ^b&0}VHdf@_ zJN;|^#UnY#c_@s(p+X&@{%yKAka=S}EexmrHEqUd8~+`Z%$e%5(>?D#m)vo)(`wR#2i3WgX)Z&j1)a6!hIcp>?v!{v{~QUrWF_&; zQ<)Sr*7Ij*4^#5&5k>Qd#r&C_&h`*gDbv~Gpt0@pned20DGYQE} z(T;fCz(~`SlljA{L=w;HU>{bqgZ=qacpTMZ;?qf-&h{cE#puoza{fa-e?pnI)A>V9 z^QRb8nT}9WThEG$F;l-pU&gv-9yD%euszawO~&jLGn{rujeT20BOD zsS8DMH8uio{Wv?9I!sW!66DW$%i%TG<;1CEdVsP^UhqDzIcYVi=pxdWXT&iJZX&uU zCm0R}c|bC&w9g)4ER_fQquR, 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 \n" +"Language-Team: LANGUAGE \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 direct link." +msgstr "" +"Twój plik jest gotowy!\n" +"Jeśli pobieranie nie zacznie się w ciągu kilku sekund,\n" +"skorzystaj z tego bezpośredniego linku." + +#: templates/waiter/wait.html:45 +#, python-format +msgid "The file you requested was: %(d)s." +msgstr "Zamówiony plik to: %(d)s." + +#: templates/waiter/wait.html:47 +msgid "" +"Be aware: Generating the file can take a while.\n" +" Please be patient, or bookmark this page and come back later.

" +msgstr "" +"Uwaga: Generowanie pliku może trwać dłuższą chwilę.\n" +"Poczekaj cierpliwie, albo dodaj tę stronę do zakładek i wróć później.

" + +#: templates/waiter/wait.html:55 +#, python-format +msgid "" +"Something seems to have gone wrong while generating your file.\n" +" Please order it again or complain to us 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 napisz do nas." + diff --git a/apps/waiter/models.py b/apps/waiter/models.py index fd9730131..26a9a6d4d 100644 --- a/apps/waiter/models.py +++ b/apps/waiter/models.py @@ -1,7 +1,9 @@ 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.core.urlresolvers import reverse +from djcelery.models import TaskMeta + 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) - # 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)] - print abs_path 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. + + 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: - # TODO: makedirs + if created or waited.is_stale(): waited.task = task_creator(cls.abspath(path)) - print waited.task 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) diff --git a/apps/waiter/templates/waiter/wait.html b/apps/waiter/templates/waiter/wait.html index 7c5a88428..f4dedc77a 100644 --- a/apps/waiter/templates/waiter/wait.html +++ b/apps/waiter/templates/waiter/wait.html @@ -1,21 +1,92 @@ {% 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 %} - {# TODO: Not like that! What are you, caveman?! #} - +{% if file_url %} + +{% else %} + {% if waiting %} + + {% endif %} +{% endif %} {% endblock %} {% block body %} - {% if file_url %} -

Plik jest gotowy! Jeśli pobieranie nie zacznie się automatycznie, - oto bezpośredni link.

- {% else %} -

Idź na kawę. To trochę potrwa.

- -

{{ waiting_for.description }}

- {% endif %} +{% if file_url %} +

{% trans "The file is ready for download!" %}

+ +
+

{% blocktrans %}Your file is ready! + If the download doesn't start in a few seconds, + feel free to use this direct link.{% endblocktrans %}

+
+{% else %} + {% if waiting %} +

{% trans "Your file is being prepared, please wait." %}

+ +
+

{% blocktrans with d=waiting.description %}The file you requested was: {{d}}.{% endblocktrans %}

+ +

{% blocktrans %}Be aware: Generating the file can take a while. + Please be patient, or bookmark this page and come back later.

{% endblocktrans %} +
+ {% else %} +

{% trans "Something went wrong." %}

+ +
+ {% url 'suggest' as s %} +

{% blocktrans %}Something seems to have gone wrong while generating your file. + Please order it again or complain to us about it.{% endblocktrans %}

+
+ {% endif %} +{% endif %} + +{% endblock %} + +{% block extrabody %} +{% if waiting %} + +{% endif %} {% endblock %} diff --git a/apps/waiter/views.py b/apps/waiter/views.py index 81cd3b49d..e38bd8f1d 100644 --- a/apps/waiter/views.py +++ b/apps/waiter/views.py @@ -1,12 +1,19 @@ 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: - 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()) diff --git a/lib/librarian b/lib/librarian index 9e13b0c99..b8e34e0e6 160000 --- a/lib/librarian +++ b/lib/librarian @@ -1 +1 @@ -Subproject commit 9e13b0c994e9d481008bef7006a74609adfd16f8 +Subproject commit b8e34e0e6730ef76a353a15ff653faa9e8c88a77 diff --git a/wolnelektury/settings/celery.py b/wolnelektury/settings/celery.py index cb5dde97d..ad37707ee 100644 --- a/wolnelektury/settings/celery.py +++ b/wolnelektury/settings/celery.py @@ -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/custom.py b/wolnelektury/settings/custom.py index 3031f4ed8..97d958802 100644 --- a/wolnelektury/settings/custom.py +++ b/wolnelektury/settings/custom.py @@ -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_CUSTOMPDF_RATE_LIMIT = '6/m' -- 2.20.1