From: Jan Szejko Date: Thu, 14 Dec 2017 08:43:33 +0000 (+0100) Subject: Merge branch 'api' X-Git-Url: https://git.mdrn.pl/wolnelektury.git/commitdiff_plain/63f861861578b33416a0b2d801252b27443fabde?hp=5074895f7d96cbd5fda6a4d566ddb25533137fd4 Merge branch 'api' --- diff --git a/src/api/handlers.py b/src/api/handlers.py index 827cd7c5f..eb56e2014 100644 --- a/src/api/handlers.py +++ b/src/api/handlers.py @@ -7,6 +7,7 @@ import json from django.contrib.sites.models import Site from django.core.urlresolvers import reverse 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 @@ -40,13 +41,25 @@ for k, v in category_singular.items(): book_tag_categories = ['author', 'epoch', 'kind', 'genre'] -def read_tags(tags, allowed): +def read_tags(tags, request, allowed): """ Reads a path of filtering tags. :param str tags: a path of category and slug pairs, like: authors/an-author/... :returns: list of Tag objects :raises: ValueError when tags can't be found """ + + def process(category, slug): + if category == 'book': + try: + books.append(Book.objects.get(slug=slug)) + except Book.DoesNotExist: + raise ValueError('Unknown book.') + try: + real_tags.append(Tag.objects.get(category=category, slug=slug)) + except Tag.DoesNotExist: + raise ValueError('Tag not found') + if not tags: return [], [] @@ -64,17 +77,14 @@ def read_tags(tags, allowed): if category not in allowed: raise ValueError('Category not allowed.') - - if category == 'book': - try: - books.append(Book.objects.get(slug=slug)) - except Book.DoesNotExist: - raise ValueError('Unknown book.') - - try: - real_tags.append(Tag.objects.get(category=category, slug=slug)) - except Tag.DoesNotExist: - raise ValueError('Tag not found') + process(category, slug) + + for key in request.GET: + if key in category_singular: + category = category_singular[key] + if category in allowed: + for slug in request.GET.getlist(key): + process(category, slug) return real_tags, books @@ -113,14 +123,12 @@ class BookDetails(object): @classmethod def url(cls, book): """ Returns Book's URL on the site. """ - return WL_BASE + book.get_absolute_url() @classmethod def children(cls, book): """ Returns all children for a book. """ - - return book.children.all() + return book.children.order_by('parent_number', 'sort_key') @classmethod def media(cls, book): @@ -136,6 +144,11 @@ class BookDetails(object): return MEDIA_BASE + default.backend.get_thumbnail( book.cover, "139x193").url if book.cover else '' + @classmethod + def cover_source_image(cls, book): + url = book.cover_source() + return url.rstrip('/') + '/file/' + class BookDetailHandler(BaseHandler, BookDetails): """ Main handler for Book objects. @@ -144,7 +157,7 @@ class BookDetailHandler(BaseHandler, BookDetails): """ allowed_methods = ['GET'] fields = ['title', 'parent', 'children'] + Book.formats + [ - 'media', 'url', 'cover', 'cover_thumb'] + [ + 'media', 'url', 'cover', 'cover_thumb', 'fragment_data'] + [ category_plural[c] for c in book_tag_categories] @piwik_track @@ -163,7 +176,7 @@ class AnonymousBooksHandler(AnonymousBaseHandler, BookDetails): """ allowed_methods = ('GET',) model = Book - fields = book_tag_categories + ['href', 'title', 'url', 'cover', 'cover_thumb'] + fields = book_tag_categories + ['href', 'title', 'url', 'cover', 'cover_thumb', 'slug'] @classmethod def genres(cls, book): @@ -171,7 +184,9 @@ class AnonymousBooksHandler(AnonymousBaseHandler, BookDetails): return book.tags.filter(category='genre') @piwik_track - def read(self, request, tags=None, top_level=False, audiobooks=False, daisy=False, pk=None): + def read(self, request, tags=None, top_level=False, audiobooks=False, daisy=False, pk=None, + recommended=False, newest=False, books=None, + after=None, before=None, count=None): """ Lists all books with given tags. :param tags: filtering tags; should be a path of categories @@ -187,10 +202,17 @@ class AnonymousBooksHandler(AnonymousBaseHandler, BookDetails): return rc.NOT_FOUND try: - tags, _ancestors = read_tags(tags, allowed=book_tag_categories) + tags, _ancestors = read_tags(tags, request, allowed=book_tag_categories) except ValueError: return rc.NOT_FOUND + if 'after' in request.GET: + after = request.GET['after'] + if 'before' in request.GET: + before = request.GET['before'] + if 'count' in request.GET: + count = request.GET['count'] + if tags: if top_level: books = Book.tagged_top_level(tags) @@ -198,7 +220,8 @@ class AnonymousBooksHandler(AnonymousBaseHandler, BookDetails): else: books = Book.tagged.with_all(tags) else: - books = Book.objects.all() + books = books if books is not None else Book.objects.all() + books = books.order_by('slug') if top_level: books = books.filter(parent=None) @@ -206,14 +229,27 @@ class AnonymousBooksHandler(AnonymousBaseHandler, BookDetails): books = books.filter(media__type='mp3').distinct() if daisy: books = books.filter(media__type='daisy').distinct() + if recommended: + books = books.filter(recommended=True) + if newest: + books = books.order_by('-created_at') + + if after: + books = books.filter(slug__gt=after) + if before: + books = books.filter(slug__lt=before) books = books.only('slug', 'title', 'cover', 'cover_thumb') for category in book_tag_categories: books = prefetch_relations(books, category) - if books: - return books - else: - return rc.NOT_FOUND + + if count: + if before: + books = list(reversed(books.order_by('-slug')[:count])) + else: + books = books[:count] + + return books def create(self, request, *args, **kwargs): return rc.FORBIDDEN @@ -222,7 +258,7 @@ class AnonymousBooksHandler(AnonymousBaseHandler, BookDetails): class BooksHandler(BookDetailHandler): allowed_methods = ('GET', 'POST') model = Book - fields = book_tag_categories + ['href', 'title', 'url', 'cover', 'cover_thumb'] + fields = book_tag_categories + ['href', 'title', 'url', 'cover', 'cover_thumb', 'slug'] anonymous = AnonymousBooksHandler def create(self, request, *args, **kwargs): @@ -239,7 +275,95 @@ class BooksHandler(BookDetailHandler): class EBooksHandler(AnonymousBooksHandler): - fields = ('author', 'href', 'title', 'cover') + tuple(Book.ebook_formats) + fields = ('author', 'href', 'title', 'cover') + tuple(Book.ebook_formats) + ('slug',) + + +class BookProxy(models.Model): + def __init__(self, book, key): + self.book = book + self.key = key + + def __getattr__(self, item): + if item not in ('book', 'key'): + return self.book.__getattribute__(item) + else: + return self.__getattribute__(item) + + +class QuerySetProxy(models.QuerySet): + def __init__(self, l): + self.list = l + + def __iter__(self): + return iter(self.list) + + +class FilterBooksHandler(AnonymousBooksHandler): + fields = book_tag_categories + [ + 'href', 'title', 'url', 'cover', 'cover_thumb', 'key', 'cover_source_image'] + + def read(self, request): + key_sep = '$' + search_string = request.GET.get('search') + is_lektura = request.GET.get('lektura') + is_audiobook = request.GET.get('audiobook') + + 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) + if is_audiobook is not None: + if is_audiobook: + books = books.filter(media__type='mp3') + else: + books = books.exclude(media__type='mp3') + for key in request.GET: + if key in category_singular: + category = category_singular[key] + if category in book_tag_categories: + slugs = request.GET[key].split(',') + tags = Tag.objects.filter(category=category, slug__in=slugs) + books = Book.tagged.with_any(tags, books) + if (search_string is not None) and len(search_string) < 3: + search_string = None + if search_string: + books_author = books.filter(cached_author__iregex='\m' + search_string) + books_title = books.filter(title__iregex='\m' + search_string) + books_title = books_title.exclude(id__in=list(books_author.values_list('id', flat=True))) + if after and (key_sep in after): + which, slug = after.split(key_sep, 1) + if which == 'title': + book_lists = [(books_title.filter(slug__gt=slug), 'title')] + else: # which == 'author' + book_lists = [(books_author.filter(slug__gt=slug), 'author'), (books_title, 'title')] + else: + book_lists = [(books_author, 'author'), (books_title, 'title')] + else: + if after and key_sep in after: + which, slug = after.split(key_sep, 1) + books = books.filter(slug__gt=slug) + book_lists = [(books, 'book')] + + filtered_books = [] + for book_list, label in book_lists: + book_list = book_list.only('slug', 'title', 'cover', 'cover_thumb') + for category in book_tag_categories: + book_list = prefetch_relations(book_list, category) + remaining_count = count - len(filtered_books) + new_books = [BookProxy(book, '%s%s%s' % (label, key_sep, book.slug)) + for book in book_list[:remaining_count]] + filtered_books += new_books + if len(filtered_books) == count: + break + + return QuerySetProxy(filtered_books) # add categorized tags fields for Book @@ -375,7 +499,7 @@ class TagsHandler(BaseHandler, TagDetails): """ allowed_methods = ('GET',) model = Tag - fields = ['name', 'href', 'url'] + fields = ['name', 'href', 'url', 'slug'] @piwik_track def read(self, request, category=None, pk=None): @@ -391,11 +515,24 @@ class TagsHandler(BaseHandler, TagDetails): except KeyError: return rc.NOT_FOUND - tags = Tag.objects.filter(category=category_sng).exclude(items=None) - if tags.exists(): - return tags - else: - return rc.NOT_FOUND + after = request.GET.get('after') + before = request.GET.get('before') + count = request.GET.get('count') + + tags = Tag.objects.filter(category=category_sng).exclude(items=None).order_by('slug') + + if after: + tags = tags.filter(slug__gt=after) + if before: + tags = tags.filter(slug__lt=before) + + if count: + if before: + tags = list(reversed(tags.order_by('-slug')[:count])) + else: + tags = tags[:count] + + return tags class FragmentDetails(object): diff --git a/src/api/urls.py b/src/api/urls.py index 4452aa2ae..310460e04 100644 --- a/src/api/urls.py +++ b/src/api/urls.py @@ -17,6 +17,7 @@ book_list_resource = CsrfExemptResource(handler=handlers.BooksHandler, authentic ebook_list_resource = Resource(handler=handlers.EBooksHandler) # book_list_resource = Resource(handler=handlers.BooksHandler) book_resource = Resource(handler=handlers.BookDetailHandler) +filter_book_resource = Resource(handler=handlers.FilterBooksHandler) collection_resource = Resource(handler=handlers.CollectionDetailHandler) collection_list_resource = Resource(handler=handlers.CollectionsHandler) @@ -30,6 +31,10 @@ fragment_list_resource = Resource(handler=handlers.FragmentsHandler) picture_resource = CsrfExemptResource(handler=handlers.PictureHandler, authentication=auth) +tags_re = r'^(?P(?:(?:[a-z0-9-]+/){2}){0,6})' +paginate_re = r'(?:before/(?P[a-z0-9-]+)/)?(?:after/(?P[a-z0-9-]+)/)?(?:count/(?P[0-9]+)/)?$' + + @ssi_included def incl(request, model, pk, emitter_format): resource = { @@ -73,19 +78,23 @@ urlpatterns = patterns( fragment_resource, name="api_fragment"), # books by tags - url(r'^(?P(?:(?:[a-z0-9-]+/){2}){0,6})books/$', + url(tags_re + r'books/' + paginate_re, book_list_resource, name='api_book_list'), - url(r'^(?P(?:(?:[a-z0-9-]+/){2}){0,6})ebooks/$', + url(tags_re + r'ebooks/' + paginate_re, ebook_list_resource, name='api_ebook_list'), - url(r'^(?P(?:(?:[a-z0-9-]+/){2}){0,6})parent_books/$', + url(tags_re + r'parent_books/' + paginate_re, book_list_resource, {"top_level": True}, name='api_parent_book_list'), - url(r'^(?P(?:(?:[a-z0-9-]+/){2}){0,6})parent_ebooks/$', + url(tags_re + r'parent_ebooks/' + paginate_re, ebook_list_resource, {"top_level": True}, name='api_parent_ebook_list'), - url(r'^(?P(?:(?:[a-z0-9-]+/){2}){0,6})audiobooks/$', + url(tags_re + r'audiobooks/' + paginate_re, book_list_resource, {"audiobooks": True}, name='api_audiobook_list'), - url(r'^(?P(?:(?:[a-z0-9-]+/){2}){0,6})daisy/$', + url(tags_re + r'daisy/' + paginate_re, book_list_resource, {"daisy": True}, name='api_daisy_list'), + url(r'^recommended/' + paginate_re, book_list_resource, {"recommended": True}, name='api_recommended_list'), + url(r'^newest/', book_list_resource, {"newest": True, "count": 20}, name='api_newest_list'), + url(r'^filter-books/', filter_book_resource, name='api_filter_books'), + url(r'^pictures/$', picture_resource), # fragments by book, tags, themes diff --git a/src/catalogue/migrations/0016_auto_20171031_1232.py b/src/catalogue/migrations/0016_auto_20171031_1232.py new file mode 100644 index 000000000..5892a2991 --- /dev/null +++ b/src/catalogue/migrations/0016_auto_20171031_1232.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +def refresh_books(apps, schema_editor): + Book = apps.get_model('catalogue', 'Book') + TagRelation = apps.get_model('catalogue', 'TagRelation') + db_alias = schema_editor.connection.alias + for book in Book.objects.using(db_alias).all(): + book.cached_author = ', '.join( + TagRelation.objects.filter(content_type__model='book', object_id=book.id, tag__category='author') + .values_list('tag__name', flat=True)) + book.has_audience = 'audience' in book.extra_info + book.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ('catalogue', '0015_book_recommended'), + ] + + operations = [ + migrations.AddField( + model_name='book', + name='cached_author', + field=models.CharField(db_index=True, max_length=240, blank=True), + ), + migrations.AddField( + model_name='book', + name='has_audience', + field=models.BooleanField(default=False), + ), + migrations.RunPython(refresh_books, migrations.RunPython.noop), + ] diff --git a/src/catalogue/models/book.py b/src/catalogue/models/book.py index 02278de67..140ba50f9 100644 --- a/src/catalogue/models/book.py +++ b/src/catalogue/models/book.py @@ -81,6 +81,9 @@ class Book(models.Model): parent = models.ForeignKey('self', blank=True, null=True, related_name='children') ancestor = models.ManyToManyField('self', blank=True, editable=False, related_name='descendant', symmetrical=False) + cached_author = models.CharField(blank=True, max_length=240, db_index=True) + has_audience = models.BooleanField(default=False) + objects = models.Manager() tagged = managers.ModelTaggedItemManager(Tag) tags = managers.TagDescriptor(Tag) @@ -123,7 +126,7 @@ class Book(models.Model): return split_tags(self.tags.exclude(category__in=('set', 'theme'))) def author_unicode(self): - return self.tag_unicode('author') + return self.cached_author def translator(self): translators = self.extra_info.get('translators') @@ -136,6 +139,9 @@ class Book(models.Model): others = '' return ', '.join(u'\xa0'.join(reversed(translator.split(', ', 1))) for translator in translators) + others + def cover_source(self): + return self.extra_info.get('cover_source', self.parent.cover_source() if self.parent else '') + def save(self, force_insert=False, force_update=False, **kwargs): from sortify import sortify @@ -148,6 +154,9 @@ class Book(models.Model): author = u'' self.sort_key_author = author + self.cached_author = self.tag_unicode('author') + self.has_audience = 'audience' in self.extra_info + ret = super(Book, self).save(force_insert, force_update, **kwargs) return ret @@ -636,6 +645,13 @@ class Book(models.Model): else: return None + def fragment_data(self): + fragment = self.choose_fragment() + if fragment: + return {'title': fragment.book.pretty_title(), 'html': fragment.get_short_text()} + else: + return None + def update_popularity(self): count = self.tags.filter(category='set').values('user').order_by('user').distinct().count() try: diff --git a/src/catalogue/tasks.py b/src/catalogue/tasks.py index 30bc55fe2..265897f52 100644 --- a/src/catalogue/tasks.py +++ b/src/catalogue/tasks.py @@ -2,14 +2,13 @@ # 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 datetime from traceback import print_exc from celery.task import task from celery.utils.log import get_task_logger from django.conf import settings +from django.utils import timezone from catalogue.utils import gallery_path -from wolnelektury.utils import localtime_to_utc from waiter.models import WaitedFile task_logger = get_task_logger(__name__) @@ -18,7 +17,7 @@ task_logger = get_task_logger(__name__) # TODO: move to model? def touch_tag(tag): update_dict = { - 'changed_at': localtime_to_utc(datetime.now()), + 'changed_at': timezone.now(), } type(tag).objects.filter(pk=tag.pk).update(**update_dict) diff --git a/src/contact/views.py b/src/contact/views.py index 82e0347ca..7ec05052a 100644 --- a/src/contact/views.py +++ b/src/contact/views.py @@ -1,28 +1,37 @@ # -*- coding: utf-8 -*- from urllib import unquote +from datetime import datetime from django.contrib.auth.decorators import permission_required from django.http import Http404 from django.shortcuts import get_object_or_404, redirect, render +from django.utils import timezone +from django.views.decorators.cache import never_cache from fnpdjango.utils.views import serve_file from honeypot.decorators import check_honeypot +from wolnelektury.utils import localtime_to_utc from .forms import contact_forms from .models import Attachment, Contact @check_honeypot +@never_cache def form(request, form_tag, force_enabled=False): try: form_class = contact_forms[form_tag] except KeyError: raise Http404 - if (getattr(form_class, 'disabled', False) and - not (force_enabled and request.user.is_superuser)): - template = getattr(form_class, 'disabled_template', None) - if template: - return render(request, template, {'title': form_class.form_title}) - raise Http404 + if not (force_enabled and request.user.is_superuser): + disabled = getattr(form_class, 'disabled', False) + end_tuple = getattr(form_class, 'ends_on', None) + end_time = localtime_to_utc(datetime(*end_tuple)) if end_tuple else None + expired = end_time and end_time < timezone.now() + if disabled or expired: + template = getattr(form_class, 'disabled_template', None) + if template: + return render(request, template, {'title': form_class.form_title}) + raise Http404 if request.method == 'POST': form = form_class(request.POST, request.FILES) else: diff --git a/src/stats/utils.py b/src/stats/utils.py index 474d1b59a..fc473a6f0 100644 --- a/src/stats/utils.py +++ b/src/stats/utils.py @@ -9,6 +9,9 @@ from functools import update_wrapper import urllib from random import random from inspect import isclass + +from django.utils.encoding import force_str + from .tasks import track_request logger = logging.getLogger(__name__) @@ -18,10 +21,10 @@ def piwik_url(request): return urllib.urlencode(dict( idsite=getattr(settings, 'PIWIK_SITE_ID', '0'), rec=1, - url='http://%s%s' % (request.META['HTTP_HOST'], request.path), + url=force_str('http://%s%s' % (request.META['HTTP_HOST'], request.path)), rand=int(random() * 0x10000), apiv=PIWIK_API_VERSION, - urlref=request.META.get('HTTP_REFERER', ''), + urlref=force_str(request.META.get('HTTP_REFERER', '')), ua=request.META.get('HTTP_USER_AGENT', ''), lang=request.META.get('HTTP_ACCEPT_LANGUAGE', ''), token_auth=getattr(settings, 'PIWIK_TOKEN', ''), diff --git a/src/wolnelektury/contact_forms.py b/src/wolnelektury/contact_forms.py index 6ca78d619..f280dec74 100644 --- a/src/wolnelektury/contact_forms.py +++ b/src/wolnelektury/contact_forms.py @@ -16,6 +16,8 @@ class KonkursForm(ContactForm): form_tag = 'konkurs' form_title = u"Konkurs Trzy strony" admin_list = ['podpis', 'contact', 'temat'] + ends_on = (2017, 11, 8) + disabled_template = 'contact/disabled_contact_form.html' opiekun_header = HeaderField(label=u'Dane\xa0Opiekuna/Opiekunki') opiekun_nazwisko = forms.CharField(label=u'Imię i nazwisko', max_length=128) @@ -66,3 +68,34 @@ class KonkursForm(ContactForm): help_text=u'Wyrażam zgodę oraz potwierdzam, że autor/ka opowiadania (lub ich przedstawiciele ustawowi – ' u'gdy dotyczy) wyrazili zgodę na fotografowanie i nagrywanie podczas gali wręczenia nagród i następnie ' u'rozpowszechnianie ich wizerunków.') + + +class WorkshopsForm(ContactForm): + form_tag = 'warsztaty' + form_title = u"Wolne Lektury Fest" + nazwisko = forms.CharField(label=u'Imię i nazwisko uczestnika', max_length=128) + instytucja = forms.CharField(label=u'Instytucja/organizacja', max_length=128, required=False) + contact = forms.EmailField(label=u'Adres e-mail', max_length=128) + tel = forms.CharField(label=u'Numer telefonu', max_length=32) + warsztat = forms.ChoiceField(choices=( + ('skad-i-jak', u'Skąd i jak bezpiecznie korzystać z darmowych i wolnych wideo i zdjęć w sieci? ' + u'Jak wykorzystać wolne licencje by zwiększyć zasięg Twoich publikacji?'), + ('jak-badac', u'Jak badać wykorzystanie zbiorów domeny publicznej?'), + ('kultura', u'Kultura dostępna dla wszystkich')), + widget=forms.RadioSelect, + ) + agree_header = HeaderField(label=mark_safe_lazy(u'Oświadczenia')) + agree_data = forms.BooleanField( + label='Przetwarzanie danych osobowych', + help_text=u'Oświadczam, że wyrażam zgodę na przetwarzanie danych osobowych zawartych w niniejszym formularzu ' + u'zgłoszeniowym przez Fundację Nowoczesna Polska (administratora danych) z siedzibą w Warszawie (00-514) ' + u'przy ul. Marszałkowskiej 84/92 lok. 125 na potrzeby organizacji warsztatów w ramach wydarzenia ' + u'„WOLNE LEKTURY FEST”. Jednocześnie oświadczam, że zostałam/em poinformowana/y o tym, że mam prawo ' + u'wglądu w treść swoich danych i możliwość ich poprawiania oraz że ich podanie jest dobrowolne, ' + u'ale niezbędne do dokonania zgłoszenia.') + agree_wizerunek = forms.BooleanField( + label='Rozpowszechnianie wizerunku', + help_text=u'Wyrażam zgodę na fotografowanie i nagrywanie podczas warsztatów „WOLNE LEKTURY FEST” ' + u'24.11.2017 roku i następnie rozpowszechnianie mojego wizerunku w celach promocyjnych.') + agree_gala = forms.BooleanField( + label=u'Wezmę udział w uroczystej gali o godz. 19.00.', required=False) diff --git a/src/wolnelektury/static/scss/main/form.scss b/src/wolnelektury/static/scss/main/form.scss index 3695d2881..d3c763838 100755 --- a/src/wolnelektury/static/scss/main/form.scss +++ b/src/wolnelektury/static/scss/main/form.scss @@ -23,4 +23,9 @@ form table { font-size: .9em; font-style: italic; } + + ul { + list-style: none; + padding-left: 0; + } }