From 6e3e08e5354baaf82f7d46cbd88883e4c7426dce Mon Sep 17 00:00:00 2001 From: Jan Szejko Date: Wed, 16 May 2018 17:18:17 +0200 Subject: [PATCH] book previews - draft (no payments yet) --- src/ajaxable/utils.py | 8 ++ src/api/handlers.py | 14 ++-- src/catalogue/constants.py | 10 +++ src/catalogue/fields.py | 25 +++++- src/catalogue/forms.py | 6 +- .../commands/update_preview_status.py | 19 +++++ .../migrations/0024_auto_20180510_1407.py | 24 ++++++ src/catalogue/models/book.py | 44 ++++++++++- .../templates/catalogue/book_detail.html | 6 +- .../templates/catalogue/book_short.html | 78 ++++++++++--------- .../templates/catalogue/book_text.html | 6 +- src/catalogue/templatetags/catalogue_tags.py | 11 +++ src/catalogue/urls.py | 1 + src/catalogue/utils.py | 4 + src/catalogue/views.py | 26 ++++++- 15 files changed, 224 insertions(+), 58 deletions(-) create mode 100644 src/catalogue/management/commands/update_preview_status.py create mode 100644 src/catalogue/migrations/0024_auto_20180510_1407.py diff --git a/src/ajaxable/utils.py b/src/ajaxable/utils.py index 9fd009108..8052e6ba6 100755 --- a/src/ajaxable/utils.py +++ b/src/ajaxable/utils.py @@ -77,6 +77,11 @@ class AjaxableFormView(object): def __call__(self, request, *args, **kwargs): """A view displaying a form, or JSON if request is AJAX.""" obj = self.get_object(request, *args, **kwargs) + + response = self.validate_object(obj, request) + if response: + return response + form_args, form_kwargs = self.form_args(request, obj) if self.form_prefix: form_kwargs['prefix'] = self.form_prefix @@ -150,6 +155,9 @@ class AjaxableFormView(object): context.update(self.extra_context(request, obj)) return render_to_response(template, context, context_instance=RequestContext(request)) + def validate_object(self, obj, request): + return None + def redirect_or_refresh(self, request, path, message=None): """If the form is AJAX, refresh the page. If not, go to `path`.""" if request.is_ajax(): diff --git a/src/api/handlers.py b/src/api/handlers.py index 9e75c5387..199f030ce 100644 --- a/src/api/handlers.py +++ b/src/api/handlers.py @@ -161,7 +161,7 @@ class BookDetailHandler(BaseHandler, BookDetails): """ allowed_methods = ['GET'] fields = ['title', 'parent', 'children'] + Book.formats + [ - 'media', 'url', 'cover', 'cover_thumb', 'simple_thumb', 'simple_cover', 'fragment_data'] + [ + 'media', 'url', 'cover', 'cover_thumb', 'simple_thumb', 'simple_cover', 'fragment_data', 'preview'] + [ category_plural[c] for c in book_tag_categories] @piwik_track @@ -394,18 +394,18 @@ def add_tag_getters(): setattr(BookDetails, plural, _tags_getter(singular)) setattr(BookDetails, singular, _tag_getter(singular)) + add_tag_getters() # add fields for files in Book def _file_getter(book_format): - field = "%s_file" % book_format - @classmethod - def get_file(cls, book): - f = getattr(book, field) - if f: - return MEDIA_BASE + f.url + @staticmethod + def get_file(book): + f_url = book.media_url(book_format) + if f_url: + return MEDIA_BASE + f_url else: return '' return get_file diff --git a/src/catalogue/constants.py b/src/catalogue/constants.py index e6773d05b..c16e3f7bb 100644 --- a/src/catalogue/constants.py +++ b/src/catalogue/constants.py @@ -26,6 +26,16 @@ EBOOK_FORMATS_WITH_COVERS = ['pdf', 'epub', 'mobi'] EBOOK_FORMATS = EBOOK_FORMATS_WITHOUT_CHILDREN + EBOOK_FORMATS_WITH_CHILDREN +EBOOK_CONTENT_TYPES = { + 'html': 'text/html', + 'pdf': 'application/pdf', + 'txt': 'text/plain', + 'epub': 'application/epub+zip', + 'mobi': 'application/x-mobipocket-ebook', + 'fb2': 'text/xml', + 'xml': 'text/xml', +} + LANGUAGES_3TO2 = { 'deu': 'de', 'ger': 'de', diff --git a/src/catalogue/fields.py b/src/catalogue/fields.py index 911f857c3..1ed34e2c5 100644 --- a/src/catalogue/fields.py +++ b/src/catalogue/fields.py @@ -28,6 +28,14 @@ class EbookFieldFile(FieldFile): """Builds the ebook in a delayed task.""" return self.field.builder.delay(self.instance, self.field.attname) + def get_url(self): + return self.instance.media_url(self.field.attname.split('_')[0]) + + def set_readable(self, readable): + import os + permissions = 0o644 if readable else 0o600 + os.chmod(self.path, permissions) + class EbookField(models.FileField): """Represents an ebook file field, attachable to a model.""" @@ -91,10 +99,15 @@ class BuildEbook(Task): obj.flush_includes() return ret + def set_file_permissions(self, fieldfile): + if fieldfile.instance.preview: + fieldfile.set_readable(False) + def build(self, fieldfile): book = fieldfile.instance out = self.transform(book.wldocument(), fieldfile) fieldfile.save(None, File(open(out.get_filename())), save=False) + self.set_file_permissions(fieldfile) if book.pk is not None: type(book).objects.filter(pk=book.pk).update(**{ fieldfile.field.attname: fieldfile @@ -169,6 +182,7 @@ class BuildHtml(BuildEbook): lang = None fieldfile.save(None, ContentFile(html_output.get_string()), save=False) + self.set_file_permissions(fieldfile) type(book).objects.filter(pk=book.pk).update(**{ fieldfile.field.attname: fieldfile }) @@ -235,9 +249,14 @@ class BuildHtml(BuildEbook): return wldoc.as_html(options={'gallery': "'%s'" % gallery}) +class BuildCover(BuildEbook): + def set_file_permissions(self, fieldfile): + pass + + @BuildEbook.register('cover_thumb') @task(ignore_result=True) -class BuildCoverThumb(BuildEbook): +class BuildCoverThumb(BuildCover): @classmethod def transform(cls, wldoc, fieldfile): from librarian.cover import WLCover @@ -246,7 +265,7 @@ class BuildCoverThumb(BuildEbook): @BuildEbook.register('cover_api_thumb') @task(ignore_result=True) -class BuildCoverApiThumb(BuildEbook): +class BuildCoverApiThumb(BuildCover): @classmethod def transform(cls, wldoc, fieldfile): from librarian.cover import WLNoBoxCover @@ -255,7 +274,7 @@ class BuildCoverApiThumb(BuildEbook): @BuildEbook.register('simple_cover') @task(ignore_result=True) -class BuildSimpleCover(BuildEbook): +class BuildSimpleCover(BuildCover): @classmethod def transform(cls, wldoc, fieldfile): from librarian.cover import WLNoBoxCover diff --git a/src/catalogue/forms.py b/src/catalogue/forms.py index c1348a1e4..bcbfe5e83 100644 --- a/src/catalogue/forms.py +++ b/src/catalogue/forms.py @@ -16,6 +16,7 @@ class BookImportForm(forms.Form): book_xml_file = forms.FileField(required=False) book_xml = forms.CharField(required=False) gallery_url = forms.CharField(required=False) + days = forms.IntegerField(required=False) def clean(self): from django.core.files.base import ContentFile @@ -30,7 +31,8 @@ class BookImportForm(forms.Form): def save(self, **kwargs): return Book.from_xml_file(self.cleaned_data['book_xml_file'], overwrite=True, - remote_gallery_url=self.cleaned_data['gallery_url'], **kwargs) + remote_gallery_url=self.cleaned_data['gallery_url'], + days=self.cleaned_data['days'], **kwargs) FORMATS = [(f, f.upper()) for f in Book.ebook_formats] @@ -98,7 +100,7 @@ class CustomPDFForm(forms.Form): def save(self, *args, **kwargs): if not self.cleaned_data['cust'] and self.book.pdf_file: # Don't build with default options, just redirect to the standard file. - return {"redirect": self.book.pdf_file.url} + return {"redirect": self.book.pdf_url()} url = WaitedFile.order( self.cleaned_data['path'], lambda p, waiter_id: build_custom_pdf.delay(self.book.id, self.cleaned_data['cust'], p, waiter_id), diff --git a/src/catalogue/management/commands/update_preview_status.py b/src/catalogue/management/commands/update_preview_status.py new file mode 100644 index 000000000..cd2e8d8f2 --- /dev/null +++ b/src/catalogue/management/commands/update_preview_status.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later. +# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information. +# +from datetime import date +from django.core.management.base import BaseCommand + +from catalogue.models import Book + + +class Command(BaseCommand): + def handle(self, *args, **options): + for book in Book.objects.filter(preview=True, preview_until__lt=date.today()): + book.preview = False + book.save() + for format_ in Book.formats: + media_file = book.get_media(format_) + if media_file: + media_file.set_readable(True) diff --git a/src/catalogue/migrations/0024_auto_20180510_1407.py b/src/catalogue/migrations/0024_auto_20180510_1407.py new file mode 100644 index 000000000..819bf26ec --- /dev/null +++ b/src/catalogue/migrations/0024_auto_20180510_1407.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('catalogue', '0023_book_abstract'), + ] + + operations = [ + migrations.AddField( + model_name='book', + name='preview', + field=models.BooleanField(default=False, verbose_name='preview'), + ), + migrations.AddField( + model_name='book', + name='preview_until', + field=models.DateField(null=True, verbose_name='preview until', blank=True), + ), + ] diff --git a/src/catalogue/models/book.py b/src/catalogue/models/book.py index 3ab26c686..696b4afb0 100644 --- a/src/catalogue/models/book.py +++ b/src/catalogue/models/book.py @@ -3,6 +3,7 @@ # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information. # from collections import OrderedDict +from datetime import date from random import randint import os.path import re @@ -71,6 +72,8 @@ class Book(models.Model): wiki_link = models.CharField(blank=True, max_length=240) print_on_demand = models.BooleanField(_('print on demand'), default=False) recommended = models.BooleanField(_('recommended'), default=False) + preview = models.BooleanField(_('preview'), default=False) + preview_until = models.DateField(_('preview until'), blank=True, null=True) # files generated during publication cover = EbookField( @@ -238,6 +241,37 @@ class Book(models.Model): def get_daisy(self): return self.get_media("daisy") + def media_url(self, format_): + media = self.get_media(format_) + if media: + if self.preview: + return reverse('embargo_link', kwargs={'slug': self.slug, 'format_': format_}) + else: + return media.url + else: + return None + + def html_url(self): + return self.media_url('html') + + def pdf_url(self): + return self.media_url('pdf') + + def epub_url(self): + return self.media_url('epub') + + def mobi_url(self): + return self.media_url('mobi') + + def txt_url(self): + return self.media_url('txt') + + def fb2_url(self): + return self.media_url('fb2') + + def xml_url(self): + return self.media_url('xml') + def has_description(self): return len(self.description) > 0 has_description.short_description = _('description') @@ -310,7 +344,7 @@ class Book(models.Model): format_) field_name = "%s_file" % format_ - books = Book.objects.filter(parent=None).exclude(**{field_name: ""}) + books = Book.objects.filter(parent=None).exclude(**{field_name: ""}).exclude(preview=True) paths = [(pretty_file_name(b), getattr(b, field_name).path) for b in books.iterator()] return create_zip(paths, app_settings.FORMAT_ZIPS[format_]) @@ -333,6 +367,7 @@ class Book(models.Model): index.index.rollback() raise e + # will make problems in conjunction with paid previews def download_pictures(self, remote_gallery_url): gallery_path = self.gallery_path() # delete previous files, so we don't include old files in ebooks @@ -373,7 +408,7 @@ class Book(models.Model): @classmethod def from_text_and_meta(cls, raw_file, book_info, overwrite=False, dont_build=None, search_index=True, - search_index_tags=True, remote_gallery_url=None): + search_index_tags=True, remote_gallery_url=None, days=0): if dont_build is None: dont_build = set() dont_build = set.union(set(dont_build), set(app_settings.DONT_BUILD)) @@ -396,6 +431,9 @@ class Book(models.Model): if created: book_shelves = [] old_cover = None + book.preview = bool(days) + if book.preview: + book.preview_until = date.today() else: if not overwrite: raise Book.AlreadyExists(_('Book %s already exists') % book_slug) @@ -405,6 +443,8 @@ class Book(models.Model): # Save XML file book.xml_file.save('%s.xml' % book.slug, raw_file, save=False) + if book.preview: + book.xml_file.set_readable(False) book.language = book_info.language book.title = book_info.title diff --git a/src/catalogue/templates/catalogue/book_detail.html b/src/catalogue/templates/catalogue/book_detail.html index 4baec4b8e..26fe1678c 100644 --- a/src/catalogue/templates/catalogue/book_detail.html +++ b/src/catalogue/templates/catalogue/book_detail.html @@ -13,7 +13,7 @@ {% block bodyid %}book-detail{% endblock %} {% block body %} - {% cache 86400 book_wide book.pk %} {# book|status:user #} + {% cache 86400 book_wide book.pk book|status:user %} {% include 'catalogue/book_wide.html' %} {% endcache %} @@ -62,7 +62,9 @@ {% trans "in" %} {% source_name extra_info.source_url %} {% endif %} -
{% trans "Source XML file" %}
+ {% if book|status:user != 'closed' %} +
{% trans "Source XML file" %}
+ {% endif %} {% if extra_info.about and not hide_about %}
{% trans "Book on" %} {% trans "Editor's Platform" %} diff --git a/src/catalogue/templates/catalogue/book_short.html b/src/catalogue/templates/catalogue/book_short.html index b2a912f1a..727ffb474 100644 --- a/src/catalogue/templates/catalogue/book_short.html +++ b/src/catalogue/templates/catalogue/book_short.html @@ -97,47 +97,51 @@
{% book_shelf_tags book.pk %} - + {% else %} +

{% trans "For now this work is only available for our subscribers." %}

+ {% endif %}
{% if book.abstract %}
diff --git a/src/catalogue/templates/catalogue/book_text.html b/src/catalogue/templates/catalogue/book_text.html index 93e755f4d..f697e4ee2 100644 --- a/src/catalogue/templates/catalogue/book_text.html +++ b/src/catalogue/templates/catalogue/book_text.html @@ -60,7 +60,7 @@ {% block big-pane %}
- +
- {% cache 86400 catalogue_book_short book.pk %} + {% cache 86400 catalogue_book_short book.pk book|status:user %} {% include 'catalogue/book_short.html' %} {% endcache %}
diff --git a/src/catalogue/templatetags/catalogue_tags.py b/src/catalogue/templatetags/catalogue_tags.py index 9f5c04b16..6906f605d 100644 --- a/src/catalogue/templatetags/catalogue_tags.py +++ b/src/catalogue/templatetags/catalogue_tags.py @@ -18,6 +18,7 @@ from ssify import ssi_variable from catalogue.helpers import get_audiobook_tags from catalogue.models import Book, BookMedia, Fragment, Tag, Source from catalogue.constants import LICENSES +from catalogue.utils import is_subscribed from picture.models import Picture register = template.Library() @@ -491,3 +492,13 @@ def strip_tag(html, tag_name): # docelowo może być warto zainstalować BeautifulSoup do takich rzeczy import re return re.sub(r"<.?%s\b[^>]*>" % tag_name, "", html) + + +@register.filter +def status(book, user): + if not book.preview: + return 'open' + elif is_subscribed(user): + return 'preview' + else: + return 'closed' diff --git a/src/catalogue/urls.py b/src/catalogue/urls.py index addb0cba0..50a4a4af9 100644 --- a/src/catalogue/urls.py +++ b/src/catalogue/urls.py @@ -64,6 +64,7 @@ urlpatterns += patterns( url(r'^audiobooki/(?Pmp3|ogg|daisy|all).xml$', AudiobookFeed(), name='audiobook_feed'), + url(r'^pobierz/(?P%s).(?P[a-z0-9]*)$' % SLUG, 'embargo_link', name='embargo_link'), # zip url(r'^zip/pdf\.zip$', 'download_zip', {'format': 'pdf', 'slug': None}, 'download_zip_pdf'), diff --git a/src/catalogue/utils.py b/src/catalogue/utils.py index 2f3b94964..2145312ad 100644 --- a/src/catalogue/utils.py +++ b/src/catalogue/utils.py @@ -354,3 +354,7 @@ def gallery_path(slug): def gallery_url(slug): return '%s%s%s/' % (settings.MEDIA_URL, settings.IMAGE_DIR, slug) + + +def is_subscribed(user): + return user.is_authenticated() # TEMPORARY diff --git a/src/catalogue/views.py b/src/catalogue/views.py index 0a5ab5867..c545cdf79 100644 --- a/src/catalogue/views.py +++ b/src/catalogue/views.py @@ -26,7 +26,7 @@ from catalogue import constants from catalogue import forms from catalogue.helpers import get_top_level_related_tags from catalogue.models import Book, Collection, Tag, Fragment -from catalogue.utils import split_tags +from catalogue.utils import split_tags, is_subscribed from catalogue.models.tag import prefetch_relations from wolnelektury.utils import is_crawler @@ -307,6 +307,9 @@ def player(request, slug): def book_text(request, slug): book = get_object_or_404(Book, slug=slug) + if book.preview and not is_subscribed(request.user): + return HttpResponseRedirect(book.get_absolute_url()) + if not book.has_html_file(): raise Http404 return render_to_response('catalogue/book_text.html', {'book': book}, context_instance=RequestContext(request)) @@ -352,6 +355,18 @@ def tag_info(request, tag_id): return HttpResponse(tag.description) +def embargo_link(request, format_, slug): + book = get_object_or_404(Book, slug=slug) + if format_ not in Book.formats: + raise Http404 + media_file = book.get_media(format_) + if not book.preview: + return HttpResponseRedirect(media_file.url) + if not is_subscribed(request.user): + return HttpResponseRedirect(book.get_absolute_url()) + return HttpResponse(media_file, content_type=constants.EBOOK_CONTENT_TYPES[format_]) + + def download_zip(request, format, slug=None): if format in Book.ebook_formats: url = Book.zip_format(format) @@ -379,8 +394,15 @@ class CustomPDFFormView(AjaxableFormView): """Override to parse view args and give additional args to the form.""" return (obj,), {} + def validate_object(self, obj, request): + book = obj + if book.preview and not is_subscribed(request.user): + return HttpResponseRedirect(book.get_absolute_url()) + return super(CustomPDFFormView, self).validate_object(obj, request) + def get_object(self, request, slug, *args, **kwargs): - return get_object_or_404(Book, slug=slug) + book = get_object_or_404(Book, slug=slug) + return book def context_description(self, request, obj): return obj.pretty_title() -- 2.20.1