book previews - draft (no payments yet)
authorJan Szejko <janek37@gmail.com>
Wed, 16 May 2018 15:18:17 +0000 (17:18 +0200)
committerJan Szejko <janek37@gmail.com>
Wed, 16 May 2018 15:18:17 +0000 (17:18 +0200)
15 files changed:
src/ajaxable/utils.py
src/api/handlers.py
src/catalogue/constants.py
src/catalogue/fields.py
src/catalogue/forms.py
src/catalogue/management/commands/update_preview_status.py [new file with mode: 0644]
src/catalogue/migrations/0024_auto_20180510_1407.py [new file with mode: 0644]
src/catalogue/models/book.py
src/catalogue/templates/catalogue/book_detail.html
src/catalogue/templates/catalogue/book_short.html
src/catalogue/templates/catalogue/book_text.html
src/catalogue/templatetags/catalogue_tags.py
src/catalogue/urls.py
src/catalogue/utils.py
src/catalogue/views.py

index 9fd0091..8052e6b 100755 (executable)
@@ -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():
index 9e75c53..199f030 100644 (file)
@@ -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
index e6773d0..c16e3f7 100644 (file)
@@ -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',
index 911f857..1ed34e2 100644 (file)
@@ -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
index c1348a1..bcbfe5e 100644 (file)
@@ -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 (file)
index 0000000..cd2e8d8
--- /dev/null
@@ -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 (file)
index 0000000..819bf26
--- /dev/null
@@ -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),
+        ),
+    ]
index 3ab26c6..696b4af 100644 (file)
@@ -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
index 4baec4b..26fe167 100644 (file)
@@ -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 %}
     </div>
   {% endif %}
-  <div class="white-box"><a href="{{ book.xml_file.url }}">{% trans "Source XML file" %}</a></div>
+  {% if book|status:user != 'closed' %}
+    <div class="white-box"><a href="{{ book.xml_url }}">{% trans "Source XML file" %}</a></div>
+  {% endif %}
   {% if extra_info.about and not hide_about %}
     <div class="white-box">
       {% trans "Book on" %} <a href="{{ extra_info.about }}">{% trans "Editor's Platform" %}</a>
index b2a912f..727ffb4 100644 (file)
       </div>
       {% book_shelf_tags book.pk %}
 
-      <ul class="book-box-tools">
-        <li class="book-box-read">
-          {% if book.html_file %}
-            <a href="{% url 'book_text' book.slug %}" class="downarrow">{% trans "Read online" %}</a>
-          {% endif %}
-          {% if book.print_on_demand %}
-            <a href="{{ book.ridero_link }}" class="downarrow print tlite-tooltip" title="{% trans "Cena książki w druku cyfrowym jest zależna od liczby stron.<br>Przed zakupem upewnij się, że cena druku na żądanie jest dla Ciebie odpowiednia.<br>Wszystkie nasze zasoby w wersji elektronicznej są zawsze dostępne bezpłatnie." %}">{% trans "Print on demand –" %}
-                <img src="{% static 'img/ridero.png' %}" style="height: 0.8em;"/></a>
-          {% endif %}
-        </li>
-        <li class="book-box-download">
-          <a class="downarrow">{% trans "Download" %}:</a>
-          <div class="book-box-formats">
-            {% if book.pdf_file %}
-              <span><a href="{{ book.pdf_file.url}}">PDF</a></span>
-            {% endif %}
-            {% if book.epub_file %}
-              <span><a href="{{ book.epub_file.url}}">EPUB</a></span>
+      {% if book|status:user != 'closed' %}
+        <ul class="book-box-tools">
+          <li class="book-box-read">
+            {% if book.html_file %}
+              <a href="{% url 'book_text' book.slug %}" class="downarrow">{% trans "Read online" %}</a>
             {% endif %}
-            {% if book.mobi_file %}
-              <span><a href="{{ book.mobi_file.url}}">MOBI</a></span>
+            {% if book.print_on_demand %}
+              <a href="{{ book.ridero_link }}" class="downarrow print tlite-tooltip" title="{% trans "Cena książki w druku cyfrowym jest zależna od liczby stron.<br>Przed zakupem upewnij się, że cena druku na żądanie jest dla Ciebie odpowiednia.<br>Wszystkie nasze zasoby w wersji elektronicznej są zawsze dostępne bezpłatnie." %}">{% trans "Print on demand –" %}
+                  <img src="{% static 'img/ridero.png' %}" style="height: 0.8em;"/></a>
             {% endif %}
-            {% if book.has_audio %}
-              <span><a href="{% url 'download_zip_mp3' book.slug %}">MP3</a></span>
-            {% endif %}
-            <a class="read-more-show hide" href="#">{% trans "more" %}</a>
-            <span class="read-more-content">
-              {% if  book.fb2_file %}
-                <span><a href="{{ book.fb2_file.url}}">FB2</a></span>
+          </li>
+          <li class="book-box-download">
+            <a class="downarrow">{% trans "Download" %}:</a>
+            <div class="book-box-formats">
+              {% if book.pdf_file %}
+                <span><a href="{{ book.pdf_url}}">PDF</a></span>
               {% endif %}
-              {% if  book.txt_file %}
-                <span><a href="{{ book.txt_file.url}}">TXT</a></span>
+              {% if book.epub_file %}
+                <span><a href="{{ book.epub_url}}">EPUB</a></span>
               {% endif %}
-              {% download_audio book mp3=False %}
-              <br>
-              {% custom_pdf_link_li book %}
-              <a class="read-more-hide hide" href="#">{% trans "less" %}</a>
-            </span>
-          </div>
-        </li>
-      </ul>
+              {% if book.mobi_file %}
+                <span><a href="{{ book.mobi_url}}">MOBI</a></span>
+              {% endif %}
+              {% if book.has_audio %}
+                <span><a href="{% url 'download_zip_mp3' book.slug %}">MP3</a></span>
+              {% endif %}
+              <a class="read-more-show hide" href="#">{% trans "more" %}</a>
+              <span class="read-more-content">
+                {% if  book.fb2_file %}
+                  <span><a href="{{ book.fb2_url}}">FB2</a></span>
+                {% endif %}
+                {% if  book.txt_file %}
+                  <span><a href="{{ book.txt_url}}">TXT</a></span>
+                {% endif %}
+                {% download_audio book mp3=False %}
+                <br>
+                {% custom_pdf_link_li book %}
+                <a class="read-more-hide hide" href="#">{% trans "less" %}</a>
+              </span>
+            </div>
+          </li>
+        </ul>
+      {% else %}
+        <p class="book-box-tools">{% trans "For now this work is only available for our subscribers." %}</p>
+      {% endif %}
       <div class="clearboth"></div>
       {% if book.abstract %}
         <div class="abstract more">
index 93e755f..f697e4e 100644 (file)
@@ -60,7 +60,7 @@
 
 {% block big-pane %}
   <article id="main-text">
-    <!--#include file='{{ book.html_file.url }}'-->
+    <!--#include file='{{ book.html_url }}'-->
   </article>
 
   <article id="other-text">
@@ -85,7 +85,7 @@
           {% for other_version in book.other_versions %}
             <li>
               <a class="display-other"
-                 data-other="{{ other_version.html_file.url }}"
+                 data-other="{{ other_version.html_url }}"
                  href="{% url 'book_text' other_version.slug %}">
                 {% cache 86400 book_mini_box other_version.pk %}
                   {% include 'catalogue/book_mini_box.html' with book=other_version no_link=True %}
   </div>
 
   <div class="box" id="book-short">
-    {% cache 86400 catalogue_book_short book.pk %}
+    {% cache 86400 catalogue_book_short book.pk book|status:user %}
       {% include 'catalogue/book_short.html' %}
     {% endcache %}
   </div>
index 9f5c04b..6906f60 100644 (file)
@@ -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'
index addb0cb..50a4a4a 100644 (file)
@@ -64,6 +64,7 @@ urlpatterns += patterns(
 
     url(r'^audiobooki/(?P<type>mp3|ogg|daisy|all).xml$', AudiobookFeed(), name='audiobook_feed'),
 
+    url(r'^pobierz/(?P<slug>%s).(?P<format_>[a-z0-9]*)$' % SLUG, 'embargo_link', name='embargo_link'),
 
     # zip
     url(r'^zip/pdf\.zip$', 'download_zip', {'format': 'pdf', 'slug': None}, 'download_zip_pdf'),
index 2f3b949..2145312 100644 (file)
@@ -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
index 0a5ab58..c545cdf 100644 (file)
@@ -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()