Merge branch 'preview'
authorJan Szejko <janek37@gmail.com>
Tue, 5 Jun 2018 14:46:14 +0000 (16:46 +0200)
committerJan Szejko <janek37@gmail.com>
Tue, 5 Jun 2018 15:11:50 +0000 (17:11 +0200)
# Conflicts:
# src/api/handlers.py
# src/catalogue/models/book.py
# src/catalogue/templates/catalogue/book_short.html

1  2 
src/ajaxable/utils.py
src/api/handlers.py
src/catalogue/migrations/0025_merge.py
src/catalogue/models/book.py
src/catalogue/templates/catalogue/book_short.html
src/wolnelektury/urls.py
src/wolnelektury/views.py

diff --combined src/ajaxable/utils.py
@@@ -48,7 -48,7 +48,7 @@@ def require_login(request)
  
  def placeholdized(form):
      for field in form.fields.values():
 -        field.widget.attrs['placeholder'] = field.label
 +        field.widget.attrs['placeholder'] = field.label + ('*' if field.required else '')
      return form
  
  
@@@ -77,6 -77,11 +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
          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 --combined src/api/handlers.py
@@@ -6,15 -6,18 +6,18 @@@ import jso
  
  from django.contrib.sites.models import Site
  from django.core.urlresolvers import reverse
+ from django.http.response import HttpResponse
  from django.utils.functional import lazy
  from django.db import models
  from piston.handler import AnonymousBaseHandler, BaseHandler
  from piston.utils import rc
  from sorl.thumbnail import default
  
+ from api.models import BookUserData
  from catalogue.forms import BookImportForm
  from catalogue.models import Book, Tag, BookMedia, Fragment, Collection
  from catalogue.models.tag import prefetch_relations
+ from catalogue.utils import is_subscribed
  from picture.models import Picture
  from picture.forms import PictureImportForm
  
@@@ -161,7 -164,7 +164,8 @@@ class BookDetailHandler(BaseHandler, Bo
      """
      allowed_methods = ['GET']
      fields = ['title', 'parent', 'children'] + Book.formats + [
-         'media', 'url', 'cover', 'cover_thumb', 'simple_thumb', 'simple_cover', 'fragment_data', 'audio_length'] + [
 -        'media', 'url', 'cover', 'cover_thumb', 'simple_thumb', 'simple_cover', 'fragment_data', 'preview'] + [
++        'media', 'url', 'cover', 'cover_thumb', 'simple_thumb', 'simple_cover', 'fragment_data', 'audio_length',
++        'preview'] + [
              category_plural[c] for c in book_tag_categories]
  
      @piwik_track
@@@ -180,7 -183,7 +184,7 @@@ class AnonymousBooksHandler(AnonymousBa
      """
      allowed_methods = ('GET',)
      model = Book
 -    fields = book_tag_categories + ['href', 'title', 'url', 'cover', 'cover_thumb', 'slug', 'simple_thumb']
 +    fields = book_tag_categories + ['href', 'title', 'url', 'cover', 'cover_thumb', 'slug', 'simple_thumb', 'has_audio']
  
      @classmethod
      def genres(cls, book):
@@@ -278,6 -281,18 +282,18 @@@ class BooksHandler(BookDetailHandler)
              return rc.NOT_FOUND
  
  
+ class EpubHandler(BookDetailHandler):
+     def read(self, request, slug):
+         if not is_subscribed(request.user):
+             return rc.FORBIDDEN
+         try:
+             book = Book.objects.get(slug=slug)
+         except Book.DoesNotExist:
+             return rc.NOT_FOUND
+         response = HttpResponse(book.get_media('epub'))
+         return response
  class EBooksHandler(AnonymousBooksHandler):
      fields = ('author', 'href', 'title', 'cover') + tuple(Book.ebook_formats) + ('slug',)
  
@@@ -307,22 -322,23 +323,23 @@@ class QuerySetProxy(models.QuerySet)
  
  class FilterBooksHandler(AnonymousBooksHandler):
      fields = book_tag_categories + [
 -        'href', 'title', 'url', 'cover', 'cover_thumb', 'simple_thumb', 'slug', 'key']
 +        'href', 'title', 'url', 'cover', 'cover_thumb', 'simple_thumb', 'has_audio', 'slug', 'key']
  
+     def parse_bool(self, s):
+         if s in ('true', 'false'):
+             return s == 'true'
+         else:
+             return None
      def read(self, request):
          key_sep = '$'
          search_string = request.GET.get('search')
-         is_lektura = request.GET.get('lektura')
-         is_audiobook = request.GET.get('audiobook')
+         is_lektura = self.parse_bool(request.GET.get('lektura'))
+         is_audiobook = self.parse_bool(request.GET.get('audiobook'))
+         preview = self.parse_bool(request.GET.get('preview'))
  
          after = request.GET.get('after')
          count = int(request.GET.get('count', 50))
-         if is_lektura in ('true', 'false'):
-             is_lektura = is_lektura == 'true'
-         else:
-             is_lektura = None
-         if is_audiobook in ('true', 'false'):
-             is_audiobook = is_audiobook == 'true'
          books = Book.objects.distinct().order_by('slug')
          if is_lektura is not None:
              books = books.filter(has_audience=is_lektura)
                  books = books.filter(media__type='mp3')
              else:
                  books = books.exclude(media__type='mp3')
+         if preview is not None:
+             books = books.filter(preview=preview)
          for key in request.GET:
              if key in category_singular:
                  category = category_singular[key]
@@@ -394,18 -412,18 +413,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
@@@ -415,6 -433,6 +434,7 @@@ def add_file_getters()
      for book_format in Book.formats:
          setattr(BookDetails, book_format, _file_getter(book_format))
  
++
  add_file_getters()
  
  
@@@ -606,7 -624,7 +626,7 @@@ class FragmentsHandler(BaseHandler, Fra
  
          """
          try:
--            tags, ancestors = read_tags(tags, allowed=self.categories)
++            tags, ancestors = read_tags(tags, request, allowed=self.categories)
          except ValueError:
              return rc.NOT_FOUND
          fragments = Fragment.tagged.with_all(tags).select_related('book')
@@@ -632,3 -650,62 +652,63 @@@ class PictureHandler(BaseHandler)
              return rc.CREATED
          else:
              return rc.NOT_FOUND
 -        ids = BookUserData.objects.filter(user=request.user, complete=state == 'complete').values_list('book_id', flat=True)
+ class UserDataHandler(BaseHandler):
+     model = BookUserData
+     fields = ('state',)
+     allowed_methods = ('GET', 'POST')
+     def read(self, request, slug):
+         try:
+             book = Book.objects.get(slug=slug)
+         except Book.DoesNotExist:
+             return rc.NOT_FOUND
+         if not request.user.is_authenticated():
+             return rc.FORBIDDEN
+         try:
+             data = BookUserData.objects.get(book=book, user=request.user)
+         except BookUserData.DoesNotExist:
+             return {'state': 'not_started'}
+         return data
+     def create(self, request, slug, state):
+         try:
+             book = Book.objects.get(slug=slug)
+         except Book.DoesNotExist:
+             return rc.NOT_FOUND
+         if not request.user.is_authenticated():
+             return rc.FORBIDDEN
+         if state not in ('reading', 'complete'):
+             return rc.NOT_FOUND
+         data, created = BookUserData.objects.get_or_create(book=book, user=request.user)
+         data.state = state
+         data.save()
+         return data
+ class UserShelfHandler(BookDetailHandler):
+     fields = book_tag_categories + [
+         'href', 'title', 'url', 'cover', 'cover_thumb', 'simple_thumb', 'slug', 'key']
+     def parse_bool(self, s):
+         if s in ('true', 'false'):
+             return s == 'true'
+         else:
+             return None
+     def read(self, request, state):
+         if not request.user.is_authenticated():
+             return rc.FORBIDDEN
+         if state not in ('reading', 'complete'):
+             return rc.NOT_FOUND
+         after = request.GET.get('after')
+         count = int(request.GET.get('count', 50))
++        ids = BookUserData.objects.filter(user=request.user, complete=state == 'complete')\
++            .values_list('book_id', flat=True)
+         books = Book.objects.filter(id__in=list(ids)).distinct().order_by('slug')
+         if after:
+             books = books.filter(slug__gt=after)
+         if count:
+             books = books[:count]
+         return books
index 0000000,0000000..66c8c8f
new file mode 100644 (file)
--- /dev/null
--- /dev/null
@@@ -1,0 -1,0 +1,15 @@@
++# -*- coding: utf-8 -*-
++from __future__ import unicode_literals
++
++from django.db import migrations, models
++
++
++class Migration(migrations.Migration):
++
++    dependencies = [
++        ('catalogue', '0024_book_audio_length'),
++        ('catalogue', '0024_auto_20180510_1407'),
++    ]
++
++    operations = [
++    ]
@@@ -3,6 -3,7 +3,7 @@@
  # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
  #
  from collections import OrderedDict
+ from datetime import date, timedelta
  from random import randint
  import os.path
  import re
@@@ -71,7 -72,8 +72,9 @@@ 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)
 +    audio_length = models.CharField(_('audio length'), blank=True, max_length=8)
+     preview = models.BooleanField(_('preview'), default=False)
+     preview_until = models.DateField(_('preview until'), blank=True, null=True)
  
      # files generated during publication
      cover = EbookField(
      def is_foreign(self):
          return self.language_code() != settings.LANGUAGE_CODE
  
 +    def set_audio_length(self):
 +        length = self.get_audio_length()
 +        if length > 0:
 +            self.audio_length = self.format_audio_length(length)
 +            self.save()
 +
 +    @staticmethod
 +    def format_audio_length(seconds):
 +        if seconds < 60*60:
 +            minutes = seconds // 60
 +            seconds = seconds % 60
 +            return '%d:%02d' % (minutes, seconds)
 +        else:
 +            hours = seconds // 3600
 +            minutes = seconds % 3600 // 60
 +            seconds = seconds % 60
 +            return '%d:%02d:%02d' % (hours, minutes, seconds)
 +
 +    def get_audio_length(self):
 +        from mutagen.mp3 import MP3
 +        total = 0
 +        for media in self.get_mp3():
 +            audio = MP3(media.file.path)
 +            total += audio.info.length
 +        return int(total)
 +
      def has_media(self, type_):
          if type_ in Book.formats:
              return bool(getattr(self, "%s_file" % type_))
      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')
      has_description.boolean = True
  
 -    # ugly ugly ugly
      def has_mp3_file(self):
 -        return bool(self.has_media("mp3"))
 +        return self.has_media("mp3")
      has_mp3_file.short_description = 'MP3'
      has_mp3_file.boolean = True
  
      def has_ogg_file(self):
 -        return bool(self.has_media("ogg"))
 +        return self.has_media("ogg")
      has_ogg_file.short_description = 'OGG'
      has_ogg_file.boolean = True
  
      def has_daisy_file(self):
 -        return bool(self.has_media("daisy"))
 +        return self.has_media("daisy")
      has_daisy_file.short_description = 'DAISY'
      has_daisy_file.boolean = True
  
                  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_])
  
              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
  
      @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))
          if created:
              book_shelves = []
              old_cover = None
+             book.preview = bool(days)
+             if book.preview:
+                 book.preview_until = date.today() + timedelta(days)
          else:
              if not overwrite:
                  raise Book.AlreadyExists(_('Book %s already exists') % book_slug)
  
          # 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
@@@ -748,6 -762,6 +788,7 @@@ def add_file_fields()
              default=''
          ).contribute_to_class(Book, field_name)
  
++
  add_file_fields()
  
  
        </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">
 -          {{ book.abstract|safe }}
 -        </div>
 -      {% endif %}
        {% block book-box-extra-info %}{% endblock %}
 -      {% block box-append %}
 -      {% endblock %}
 +      {% block box-append %}{% endblock %}
      </div>
 +    {% if book.abstract %}
 +      <div class="abstract more">
 +        {{ book.abstract|safe }}
 +      </div>
 +    {% endif %}
      {% endwith %}
  
      {% block right-column %}
diff --combined src/wolnelektury/urls.py
@@@ -25,7 -25,6 +25,7 @@@ urlpatterns = 
      url(r'^uzytkownik/signup/$', views.RegisterFormView(), name='register'),
      url(r'^uzytkownik/logout/$', views.logout_then_redirect, name='logout'),
      url(r'^uzytkownik/zaloguj-utworz/$', views.LoginRegisterFormView(), name='login_register'),
 +    url(r'^uzytkownik/social/signup/$', views.SocialSignupView.as_view(), name='socialaccount_signup'),
  
      # Includes.
      url(r'^latests_blog_posts.html$', views.latest_blog_posts, name='latest_blog_posts'),
@@@ -90,4 -89,5 +90,5 @@@ urlpatterns += 
  
  urlpatterns += [
      url(r'^error-test/$', views.exception_test),
+     # url(r'^post-test/$', views.post_test),
  ]
@@@ -4,12 -4,11 +4,12 @@@
  #
  from datetime import date, datetime
  import feedparser
 +from allauth.socialaccount.views import SignupView
  
  from django.conf import settings
  from django.contrib import auth
  from django.contrib.auth.decorators import login_required
 -from django.contrib.auth.forms import UserCreationForm, AuthenticationForm
 +from django.contrib.auth.forms import AuthenticationForm
  from django.core.cache import cache
  from django.http import HttpResponse, HttpResponseRedirect
  from django.shortcuts import render
@@@ -23,7 -22,7 +23,7 @@@ from catalogue.models import Book, Coll
  from ssify import ssi_included
  
  from social.utils import get_or_choose_cite
 -from wolnelektury.forms import RegistrationForm
 +from wolnelektury.forms import RegistrationForm, SocialSignupForm
  
  
  def main_page(request):
@@@ -187,13 -186,13 +187,17 @@@ def widget(request)
      return render(request, 'widget.html')
  
  
 +class SocialSignupView(SignupView):
 +    form_class = SocialSignupForm
 +
 +
  def exception_test(request):
      msg = request.GET.get('msg')
      if msg:
          raise Exception('Exception test: %s' % msg)
      else:
-         raise Exception('Exception test')
+         raise Exception('Exception test')
+ def post_test(request):
+     return render(request, 'post_test.html', {'action': '/api/reading/jego-zasady/complete/'})