From: Jan Szejko Date: Tue, 5 Jun 2018 14:46:14 +0000 (+0200) Subject: Merge branch 'preview' X-Git-Url: https://git.mdrn.pl/wolnelektury.git/commitdiff_plain/73b6a1639ba8f9f5fc7615c136364fdba73e5bb2?hp=-c Merge branch 'preview' # Conflicts: # src/api/handlers.py # src/catalogue/models/book.py # src/catalogue/templates/catalogue/book_short.html --- 73b6a1639ba8f9f5fc7615c136364fdba73e5bb2 diff --combined src/ajaxable/utils.py index 80d398f73,8052e6ba6..82de84771 --- a/src/ajaxable/utils.py +++ b/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 @@@ -150,6 -155,9 +155,9 @@@ 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 index 893cd7e1b,c89997617..3e4e0931a --- a/src/api/handlers.py +++ b/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) @@@ -331,6 -347,8 +348,8 @@@ 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 + + + 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) ++ 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 diff --combined src/catalogue/migrations/0025_merge.py index 000000000,000000000..66c8c8f4d new file mode 100644 --- /dev/null +++ b/src/catalogue/migrations/0025_merge.py @@@ -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 = [ ++ ] diff --combined src/catalogue/models/book.py index a1ca5bbed,027d272a3..004c27e89 --- a/src/catalogue/models/book.py +++ b/src/catalogue/models/book.py @@@ -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( @@@ -209,32 -211,6 +212,32 @@@ 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_)) @@@ -265,23 -241,55 +268,54 @@@ 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 @@@ -336,7 -344,7 +370,7 @@@ 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_]) @@@ -359,6 -367,7 +393,7 @@@ 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 @@@ -399,7 -408,7 +434,7 @@@ @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)) @@@ -422,6 -431,9 +457,9 @@@ 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) @@@ -431,6 -443,8 +469,8 @@@ # 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() diff --combined src/catalogue/templates/catalogue/book_short.html index fe4871d81,727ffb474..675e761c4 --- a/src/catalogue/templates/catalogue/book_short.html +++ b/src/catalogue/templates/catalogue/book_short.html @@@ -97,55 -97,61 +97,59 @@@ {% book_shelf_tags book.pk %} - + {% else %} +

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

+ {% endif %} -
- {% if book.abstract %} -
- {{ book.abstract|safe }} -
- {% endif %} {% block book-box-extra-info %}{% endblock %} - {% block box-append %} - {% endblock %} + {% block box-append %}{% endblock %} + {% if book.abstract %} +
+ {{ book.abstract|safe }} +
+ {% endif %} {% endwith %} {% block right-column %} diff --combined src/wolnelektury/urls.py index fa011086a,ee807fd2d..bf95306d9 --- a/src/wolnelektury/urls.py +++ b/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), ] diff --combined src/wolnelektury/views.py index 28ce22374,21151510d..a5db0a83f --- a/src/wolnelektury/views.py +++ b/src/wolnelektury/views.py @@@ -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/'})