From c3e563338050cf161ed6568d01c8a5f010257c2f Mon Sep 17 00:00:00 2001 From: Radek Czajka Date: Tue, 5 Feb 2019 09:52:45 +0100 Subject: [PATCH 01/16] Preserve the weirdness of the API. --- src/api/tests/res/responses/books.json | 3 +++ src/api/tests/res/responses/books.xml | 3 +-- src/catalogue/api/serializers.py | 18 ++++++++---------- src/catalogue/api/views.py | 4 ++-- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/api/tests/res/responses/books.json b/src/api/tests/res/responses/books.json index cfd5ee3dd..4b8d11eba 100644 --- a/src/api/tests/res/responses/books.json +++ b/src/api/tests/res/responses/books.json @@ -8,6 +8,7 @@ "has_audio": false, "title": "Child", "cover": "", + "liked": null, "epoch": "", "href": "http://testserver/api/books/child/", "genre": "Wiersz", @@ -24,6 +25,7 @@ "has_audio": false, "title": "Grandchild", "cover": "", + "liked": null, "epoch": "", "href": "http://testserver/api/books/grandchild/", "genre": "Sonet", @@ -40,6 +42,7 @@ "has_audio": true, "title": "Parent", "cover": "cover/parent.jpg", + "liked": null, "epoch": "Barok", "href": "http://testserver/api/books/parent/", "genre": "Sonet", diff --git a/src/api/tests/res/responses/books.xml b/src/api/tests/res/responses/books.xml index 967f8e2ed..f39864480 100644 --- a/src/api/tests/res/responses/books.xml +++ b/src/api/tests/res/responses/books.xml @@ -1,3 +1,2 @@ -$child$2Childhttps://example.com/katalog/lektura/child/Falsechildhttps://example.com/api/books/child/Wiersz#000000$grandchild$3Grandchildhttps://example.com/katalog/lektura/grandchild/Falsegrandchildhttps://example.com/api/books/grandchild/Sonet#000000Lirykajohn doe$parent$1Parenthttps://example.com/katalog/lektura/parent/TrueJohn Doecover/parent.jpgparentBarokhttps://example.com/api/books/parent/Sonethttps://example.com/media/cover_api_thumb/parent.jpg#a6820ahttps://example.com/media/cover/parent.jpg-139x193 - +$child$2Childhttp://testserver/katalog/lektura/child/#000000http://testserver/api/books/child/FalseWierszchild$grandchild$3Grandchildhttp://testserver/katalog/lektura/grandchild/#000000http://testserver/api/books/grandchild/FalseSonetgrandchildLirykajohn doe$parent$1Parenthttp://testserver/katalog/lektura/parent/#a6820aJohn Doecover/parent.jpgBarokhttp://testserver/api/books/parent/TrueSonethttp://testserver/media/cover_api_thumb/parent.jpgparentcover_thumb/parent.jpg diff --git a/src/catalogue/api/serializers.py b/src/catalogue/api/serializers.py index e8f04ca70..ea48cdba4 100644 --- a/src/catalogue/api/serializers.py +++ b/src/catalogue/api/serializers.py @@ -24,16 +24,17 @@ class TagDetailSerializer(serializers.ModelSerializer): fields = ['name', 'url', 'sort_key', 'description'] -class BaseBookSerializer(LegacyMixin, serializers.ModelSerializer): +class BookSerializer(LegacyMixin, serializers.ModelSerializer): author = serializers.CharField(source='author_unicode') kind = serializers.CharField(source='kind_unicode') epoch = serializers.CharField(source='epoch_unicode') genre = serializers.CharField(source='genre_unicode') + liked = BookLiked() simple_thumb = serializers.FileField(source='cover_api_thumb') href = AbsoluteURLField(view_name='catalogue_api_book', view_args=['slug']) url = AbsoluteURLField() - cover = serializers.CharField() + cover = serializers.FileField() cover_thumb = ThumbnailField('139x193', source='cover') class Meta: @@ -41,20 +42,17 @@ class BaseBookSerializer(LegacyMixin, serializers.ModelSerializer): fields = [ 'kind', 'full_sort_key', 'title', 'url', 'cover_color', 'author', 'cover', 'epoch', 'href', 'has_audio', 'genre', - 'simple_thumb', 'slug', 'cover_thumb'] + 'simple_thumb', 'slug', 'cover_thumb', 'liked'] legacy_non_null_fields = [ 'kind', 'author', 'epoch', 'genre', 'cover', 'simple_thumb', 'cover_thumb'] -class BookSerializer(BaseBookSerializer): - liked = BookLiked() - cover = serializers.FileField() +class BookListSerializer(BookSerializer): + cover = serializers.CharField() + cover_thumb = serializers.CharField() - class Meta: - model = Book - fields = BaseBookSerializer.Meta.fields + ['liked'] - legacy_non_null_fields = BaseBookSerializer.Meta.legacy_non_null_fields + Meta = BookSerializer.Meta class MediaSerializer(LegacyMixin, serializers.ModelSerializer): diff --git a/src/catalogue/api/views.py b/src/catalogue/api/views.py index e21aab707..f3b719309 100644 --- a/src/catalogue/api/views.py +++ b/src/catalogue/api/views.py @@ -24,12 +24,12 @@ class CollectionDetail(RetrieveAPIView): class BookList(ListAPIView): permission_classes = [DjangoModelPermissionsOrAnonReadOnly] queryset = Book.objects.none() # Required for DjangoModelPermissions - serializer_class = serializers.BaseBookSerializer + serializer_class = serializers.BookListSerializer def get_queryset(self): try: tags, ancestors = read_tags( - self.kwargs['tags'], self.request, + self.kwargs.get('tags', ''), self.request, allowed=('author', 'epoch', 'kind', 'genre') ) except ValueError: -- 2.20.1 From ff382f6c37063c0c4c5d21c2834a8759e25c5d02 Mon Sep 17 00:00:00 2001 From: Radek Czajka Date: Tue, 5 Feb 2019 19:07:03 +0100 Subject: [PATCH 02/16] All views migrated from Piston, except for OAuth. --- src/api/fields.py | 17 + src/api/handlers.py | 438 +----------------- src/api/helpers.py | 12 - src/api/serializers.py | 34 +- src/api/tests/res/responses/ebooks.json | 22 +- src/api/tests/res/responses/filter-books.json | 23 +- src/api/tests/tests.py | 47 +- src/api/urls.py | 56 +-- src/api/views.py | 21 +- src/catalogue/api/fields.py | 17 +- src/catalogue/api/serializers.py | 26 +- src/catalogue/api/urls.py | 10 + src/catalogue/api/views.py | 88 +++- src/picture/api/__init__.py | 0 src/picture/api/urls.py | 11 + src/picture/api/views.py | 26 ++ 16 files changed, 302 insertions(+), 546 deletions(-) delete mode 100644 src/api/helpers.py create mode 100644 src/picture/api/__init__.py create mode 100644 src/picture/api/urls.py create mode 100644 src/picture/api/views.py diff --git a/src/api/fields.py b/src/api/fields.py index 1ce83cc3e..66fd7d2e9 100644 --- a/src/api/fields.py +++ b/src/api/fields.py @@ -1,4 +1,9 @@ +# -*- 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 rest_framework import serializers +from sorl.thumbnail import default from django.core.urlresolvers import reverse from paypal.rest import user_is_subscribed @@ -41,3 +46,15 @@ class UserPremiumField(serializers.ReadOnlyField): def to_representation(self, value): return user_is_subscribed(value) + + +class ThumbnailField(serializers.FileField): + def __init__(self, geometry, *args, **kwargs): + self.geometry = geometry + super(ThumbnailField, self).__init__(*args, **kwargs) + + def to_representation(self, value): + if value: + return super(ThumbnailField, self).to_representation( + default.backend.get_thumbnail(value, self.geometry) + ) diff --git a/src/api/handlers.py b/src/api/handlers.py index 7178b9f9e..d08812f41 100644 --- a/src/api/handlers.py +++ b/src/api/handlers.py @@ -2,34 +2,12 @@ # This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later. # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information. # -import json - from django.contrib.sites.models import Site -from django.core.urlresolvers import reverse -from django.db.models import Q -from django.http.response import HttpResponse from django.utils.functional import lazy -from django.db import models -from migdal.models import Entry -from piston.handler import AnonymousBaseHandler, BaseHandler -from piston.utils import rc -from sorl.thumbnail import default - -from api.models import BookUserData -from catalogue.api.helpers import books_after, order_books -from catalogue.forms import BookImportForm -from catalogue.models import Book, Tag, BookMedia -from catalogue.models.tag import prefetch_relations -from paypal.rest import user_is_subscribed -from picture.models import Picture -from picture.forms import PictureImportForm -from social.utils import likes +from catalogue.models import Book, Tag -from stats.utils import piwik_track -from wolnelektury.utils import re_escape - -API_BASE = WL_BASE = MEDIA_BASE = lazy( +WL_BASE = lazy( lambda: u'https://' + Site.objects.get_current().domain, unicode)() category_singular = { @@ -40,14 +18,6 @@ category_singular = { 'themes': 'theme', 'books': 'book', } -category_plural = {} -for k, v in category_singular.items(): - category_plural[v] = k - -book_tag_categories = ['author', 'epoch', 'kind', 'genre'] - -book_list_fields = book_tag_categories + [ - 'href', 'title', 'url', 'cover', 'cover_thumb', 'slug', 'simple_thumb', 'has_audio', 'cover_color', 'full_sort_key'] def read_tags(tags, request, allowed): @@ -96,407 +66,3 @@ def read_tags(tags, request, allowed): for slug in request.GET.getlist(key): process(category, slug) return real_tags, books - - -# RESTful handlers - - - - - -class BookDetails(object): - """Custom fields used for representing Books.""" - - @classmethod - def href(cls, book): - """ Returns an URI for a Book in the API. """ - return API_BASE + reverse("catalogue_api_book", args=[book.slug]) - - @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.order_by('parent_number', 'sort_key') - - @classmethod - def media(cls, book): - """ Returns all media for a book. """ - return book.media.all() - - @classmethod - def cover(cls, book): - return MEDIA_BASE + book.cover.url if book.cover else '' - - @classmethod - def cover_thumb(cls, book): - return MEDIA_BASE + default.backend.get_thumbnail( - book.cover, "139x193").url if book.cover else '' - - @classmethod - def simple_thumb(cls, book): - return MEDIA_BASE + book.cover_api_thumb.url if book.cover_api_thumb else '' - - @classmethod - def simple_cover(cls, book): - return MEDIA_BASE + book.simple_cover.url if book.simple_cover else '' - - -class BookDetailHandler(BaseHandler, BookDetails): - """ Main handler for Book objects. - - Responsible for single Book details. - """ - allowed_methods = ['GET'] - fields = ['title', 'parent', 'children'] + Book.formats + [ - 'media', 'url', 'cover', 'cover_thumb', 'simple_thumb', 'simple_cover', 'fragment_data', 'audio_length', - 'preview', 'cover_color'] + [ - category_plural[c] for c in book_tag_categories] - - @piwik_track - def read(self, request, book): - """ Returns details of a book, identified by a slug and lang. """ - try: - return Book.objects.get(slug=book) - except Book.DoesNotExist: - return rc.NOT_FOUND - - -class AnonymousBooksHandler(AnonymousBaseHandler, BookDetails): - """ Main handler for Book objects. - - Responsible for lists of Book objects. - """ - allowed_methods = ('GET',) - model = Book - fields = book_list_fields - - # FIXME: Unused? - @classmethod - def genres(cls, book): - """ Returns all media for a book. """ - return book.tags.filter(category='genre') - - @piwik_track - def read(self, request, tags=None, top_level=False, audiobooks=False, daisy=False, pk=None, - recommended=False, newest=False, books=None, - after=None, count=None): - """ Lists all books with given tags. - - :param tags: filtering tags; should be a path of categories - and slugs, i.e.: authors/an-author/epoch/an-epoch/ - :param top_level: if True and a book is included in the results, - it's children are aren't. By default all books matching the tags - are returned. - """ - if pk is not None: - # FIXME: Unused? - try: - return Book.objects.get(pk=pk) - except Book.DoesNotExist: - return rc.NOT_FOUND - - try: - tags, _ancestors = read_tags(tags, request, allowed=book_tag_categories) - except ValueError: - return rc.NOT_FOUND - - new_api = request.GET.get('new_api') - if 'after' in request.GET: - after = request.GET['after'] - if 'count' in request.GET: - count = request.GET['count'] - - if tags: - if top_level: - books = Book.tagged_top_level(tags) - return books if books else rc.NOT_FOUND - else: - books = Book.tagged.with_all(tags) - else: - books = books if books is not None else Book.objects.all() - books = order_books(books, new_api) - - if top_level: - books = books.filter(parent=None) - if audiobooks: - 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_after(books, after, new_api) - - if new_api: - books = books.only('slug', 'title', 'cover', 'cover_thumb', 'sort_key', 'sort_key_author') - else: - books = books.only('slug', 'title', 'cover', 'cover_thumb') - for category in book_tag_categories: - books = prefetch_relations(books, category) - - if count: - books = books[:count] - - return books - - def create(self, request, *args, **kwargs): - return rc.FORBIDDEN - - -class BooksHandler(BookDetailHandler): - allowed_methods = ('GET', 'POST') - model = Book - fields = book_list_fields + ['liked'] - anonymous = AnonymousBooksHandler - - # hack, because piston is stupid - @classmethod - def liked(cls, book): - return getattr(book, 'liked', None) - - def read(self, request, **kwargs): - books = AnonymousBooksHandler().read(request, **kwargs) - likes = set(Book.tagged.with_any(request.user.tag_set.all()).values_list('id', flat=True)) - - new_books = [ - BookProxy(book).set('liked', book.id in likes) - for book in books] - return QuerySetProxy(new_books) - - def create(self, request, *args, **kwargs): - if not request.user.has_perm('catalogue.add_book'): - return rc.FORBIDDEN - - data = json.loads(request.POST.get('data')) - form = BookImportForm(data) - if form.is_valid(): - form.save() - return rc.CREATED - else: - return rc.NOT_FOUND - - -class EBooksHandler(AnonymousBooksHandler): - fields = ('author', 'href', 'title', 'cover') + tuple(Book.ebook_formats) + ('slug',) - - -class BookProxy(models.Model): - class Meta: - managed = False - - def __init__(self, book, key=None): - self.book = book - self.key = key - - def set(self, attr, value): - self.__setattr__(attr, value) - return self - - def __getattr__(self, item): - return self.book.__getattribute__(item) - - -class QuerySetProxy(models.QuerySet): - def __init__(self, l): - self.list = l - - def __iter__(self): - return iter(self.list) - - -class AnonFilterBooksHandler(AnonymousBooksHandler): - fields = book_list_fields + ['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 = self.parse_bool(request.GET.get('lektura')) - is_audiobook = self.parse_bool(request.GET.get('audiobook')) - preview = self.parse_bool(request.GET.get('preview')) - - new_api = request.GET.get('new_api') - after = request.GET.get('after') - count = int(request.GET.get('count', 50)) - books = order_books(Book.objects.distinct(), new_api) - 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') - if preview is not None: - books = books.filter(preview=preview) - 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: - search_string = re_escape(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, key = after.split(key_sep, 1) - if which == 'title': - book_lists = [(books_after(books_title, key, new_api), 'title')] - else: # which == 'author' - book_lists = [(books_after(books_author, key, new_api), 'author'), (books_title, 'title')] - else: - book_lists = [(books_author, 'author'), (books_title, 'title')] - else: - if after and key_sep in after: - which, key = after.split(key_sep, 1) - books = books_after(books, key, new_api) - book_lists = [(books, 'book')] - - filtered_books = [] - for book_list, label in book_lists: - book_list = book_list.only('slug', 'title', 'cover', 'cover_thumb', 'sort_key_author', 'sort_key') - 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 if not new_api else book.full_sort_key())) - for book in book_list[:remaining_count]] - filtered_books += new_books - if len(filtered_books) == count: - break - - return QuerySetProxy(filtered_books) - - -class FilterBooksHandler(BooksHandler): - anonymous = AnonFilterBooksHandler - fields = book_list_fields + ['key', 'liked'] - - # hack, because piston is stupid - @classmethod - def liked(cls, book): - return getattr(book, 'liked', None) - - def read(self, request): - qsp = AnonFilterBooksHandler().read(request) - likes = set(Book.tagged.with_any(request.user.tag_set.all()).values_list('id', flat=True)) - for book in qsp.list: - book.set('liked', book.id in likes) - return qsp - - -# add categorized tags fields for Book -def _tags_getter(category): - @classmethod - def get_tags(cls, book): - return book.tags.filter(category=category) - return get_tags - - -def _tag_getter(category): - @classmethod - def get_tag(cls, book): - return book.tag_unicode(category) - return get_tag - - -def add_tag_getters(): - for plural, singular in category_singular.items(): - 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): - - @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 - - -def add_file_getters(): - for book_format in Book.formats: - setattr(BookDetails, book_format, _file_getter(book_format)) - - -add_file_getters() - - -class PictureHandler(BaseHandler): - model = Picture - fields = ('slug', 'title') - allowed_methods = ('POST',) - - def create(self, request): - if not request.user.has_perm('picture.add_picture'): - return rc.FORBIDDEN - - data = json.loads(request.POST.get('data')) - form = PictureImportForm(data) - if form.is_valid(): - form.save() - return rc.CREATED - else: - return rc.NOT_FOUND - - -class BlogEntryHandler(BaseHandler): - model = Entry - fields = ( - 'title', 'lead', 'body', 'place', 'time', 'image_url', 'image_thumb', 'gallery_urls', 'type', 'key', 'url') - - def read(self, request): - after = request.GET.get('after') - count = int(request.GET.get('count', 20)) - entries = Entry.published_objects.filter(in_stream=True).order_by('-first_published_at') - if after: - entries = entries.filter(first_published_at__lt=after) - if count: - entries = entries[:count] - return entries - - @classmethod - def image_url(cls, entry): - return (WL_BASE + entry.image.url) if entry.image else None - - @classmethod - def image_thumb(cls, entry): - return MEDIA_BASE + default.backend.get_thumbnail( - entry.image, "193x193").url if entry.image else '' - - @classmethod - def gallery_urls(cls, entry): - return [WL_BASE + photo.url() for photo in entry.photo_set.all()] - - @classmethod - def key(cls, entry): - return entry.first_published_at - - @classmethod - def url(cls, entry): - return WL_BASE + entry.get_absolute_url() diff --git a/src/api/helpers.py b/src/api/helpers.py deleted file mode 100644 index ffe92846f..000000000 --- a/src/api/helpers.py +++ /dev/null @@ -1,12 +0,0 @@ -# -*- 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 piston.resource import Resource - - -class CsrfExemptResource(Resource): - """A Custom Resource that is csrf exempt""" - def __init__(self, handler, authentication=None): - super(CsrfExemptResource, self).__init__(handler, authentication) - self.csrf_exempt = getattr(self.handler, 'csrf_exempt', True) diff --git a/src/api/serializers.py b/src/api/serializers.py index a876387e9..f2c22d4d4 100644 --- a/src/api/serializers.py +++ b/src/api/serializers.py @@ -1,7 +1,18 @@ +# -*- 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 django.contrib.auth.models import User from rest_framework import serializers -from .fields import UserPremiumField +from .fields import UserPremiumField, AbsoluteURLField, ThumbnailField from .models import BookUserData +from migdal.models import Entry, Photo + + +class PlainSerializer(serializers.ModelSerializer): + def to_representation(self, value): + value = super(PlainSerializer, self).to_representation(value) + return value.values()[0] class UserSerializer(serializers.ModelSerializer): @@ -16,3 +27,24 @@ class BookUserDataSerializer(serializers.ModelSerializer): class Meta: model = BookUserData fields = ['state'] + + +class BlogGalleryUrlSerializer(PlainSerializer): + class Meta: + model = Photo + fields = ['image'] + + +class BlogSerializer(serializers.ModelSerializer): + url = AbsoluteURLField() + image_url = serializers.FileField(source='image') + image_thumb = ThumbnailField('193x193', source='image') + key = serializers.DateTimeField(source='first_published_at') + gallery_urls = BlogGalleryUrlSerializer(many=True, source='photo_set') + body = serializers.CharField() + lead = serializers.CharField() + + class Meta: + model = Entry + fields = ['title', 'lead', 'body', 'place', 'time', 'image_url', 'image_thumb', + 'gallery_urls', 'type', 'key', 'url'] diff --git a/src/api/tests/res/responses/ebooks.json b/src/api/tests/res/responses/ebooks.json index 9c4659a1b..719faac17 100644 --- a/src/api/tests/res/responses/ebooks.json +++ b/src/api/tests/res/responses/ebooks.json @@ -5,34 +5,34 @@ "title": "Child", "author": "", "cover": "", - "href": "https://example.com/api/books/child/", + "href": "http://testserver/api/books/child/", "pdf": "", "txt": "", "slug": "child", "epub": "" }, { - "fb2": "https://example.com/katalog/pobierz/grandchild.fb2", - "mobi": "https://example.com/katalog/pobierz/grandchild.mobi", + "fb2": "http://testserver/katalog/pobierz/grandchild.fb2", + "mobi": "http://testserver/katalog/pobierz/grandchild.mobi", "title": "Grandchild", "author": "", "cover": "", - "href": "https://example.com/api/books/grandchild/", - "pdf": "https://example.com/katalog/pobierz/grandchild.pdf", - "txt": "https://example.com/katalog/pobierz/grandchild.txt", + "href": "http://testserver/api/books/grandchild/", + "pdf": "http://testserver/katalog/pobierz/grandchild.pdf", + "txt": "http://testserver/katalog/pobierz/grandchild.txt", "slug": "grandchild", - "epub": "https://example.com/katalog/pobierz/grandchild.epub" + "epub": "http://testserver/katalog/pobierz/grandchild.epub" }, { "fb2": "", - "mobi": "https://example.com/media/mobi/parent.mobi", + "mobi": "http://testserver/media/mobi/parent.mobi", "title": "Parent", "author": "John Doe", "cover": "cover/parent.jpg", - "href": "https://example.com/api/books/parent/", - "pdf": "https://example.com/media/pdf/parent.pdf", + "href": "http://testserver/api/books/parent/", + "pdf": "http://testserver/media/pdf/parent.pdf", "txt": "", "slug": "parent", - "epub": "https://example.com/media/epub/parent.epub" + "epub": "http://testserver/media/epub/parent.epub" } ] diff --git a/src/api/tests/res/responses/filter-books.json b/src/api/tests/res/responses/filter-books.json index 60ff4d3a7..49f3dea18 100644 --- a/src/api/tests/res/responses/filter-books.json +++ b/src/api/tests/res/responses/filter-books.json @@ -4,12 +4,13 @@ "full_sort_key": "$child$2", "cover_color": "#000000", "author": "", - "url": "https://example.com/katalog/lektura/child/", + "url": "http://testserver/katalog/lektura/child/", "has_audio": false, "title": "Child", - "cover": "", + "cover": "", + "liked": null, "epoch": "", - "href": "https://example.com/api/books/child/", + "href": "http://testserver/api/books/child/", "key": "book$child", "genre": "Wiersz", "simple_thumb": "", @@ -21,12 +22,13 @@ "full_sort_key": "$grandchild$3", "cover_color": "#000000", "author": "", - "url": "https://example.com/katalog/lektura/grandchild/", + "url": "http://testserver/katalog/lektura/grandchild/", "has_audio": false, "title": "Grandchild", - "cover": "", + "cover": "", + "liked": null, "epoch": "", - "href": "https://example.com/api/books/grandchild/", + "href": "http://testserver/api/books/grandchild/", "key": "book$grandchild", "genre": "Sonet", "simple_thumb": "", @@ -38,15 +40,16 @@ "full_sort_key": "john doe$parent$1", "cover_color": "#a6820a", "author": "John Doe", - "url": "https://example.com/katalog/lektura/parent/", + "url": "http://testserver/katalog/lektura/parent/", "has_audio": true, "title": "Parent", - "cover": "cover/parent.jpg", + "cover": "cover/parent.jpg", + "liked": null, "epoch": "Barok", - "href": "https://example.com/api/books/parent/", + "href": "http://testserver/api/books/parent/", "key": "book$parent", "genre": "Sonet", - "simple_thumb": "https://example.com/media/cover_api_thumb/parent.jpg", + "simple_thumb": "http://testserver/media/cover_api_thumb/parent.jpg", "slug": "parent", "cover_thumb": "cover_thumb/parent.jpg" } diff --git a/src/api/tests/tests.py b/src/api/tests/tests.py index 6be34ed6b..38f1882bb 100644 --- a/src/api/tests/tests.py +++ b/src/api/tests/tests.py @@ -327,7 +327,7 @@ class AuthorizedTests(ApiTest): cls.consumer.delete() super(AuthorizedTests, cls).tearDownClass() - def signed(self, url, method='GET', params=None): + def signed(self, url, method='GET', params=None, data=None): auth_params = { "oauth_consumer_key": self.consumer.key, "oauth_nonce": "%f" % time(), @@ -340,12 +340,14 @@ class AuthorizedTests(ApiTest): sign_params = {} if params: sign_params.update(params) + if data: + sign_params.update(data) sign_params.update(auth_params) raw = "&".join([ method.upper(), quote('http://testserver' + url, safe=''), quote("&".join( - quote(str(k)) + "=" + quote(str(v)) + quote(str(k), safe='') + "=" + quote(str(v), safe='') for (k, v) in sorted(sign_params.items()))) ]) auth_params["oauth_signature"] = quote(b64encode(hmac.new( @@ -357,11 +359,12 @@ class AuthorizedTests(ApiTest): url = url + '?' + urlencode(params) return getattr(self.client, method.lower())( url, + data=data, HTTP_AUTHORIZATION=auth ) - def signed_json(self, url, method='GET', params=None): - return json.loads(self.signed(url, method, params).content) + def signed_json(self, url, method='GET', params=None, data=None): + return json.loads(self.signed(url, method, params, data).content) def test_books(self): self.assertEqual( @@ -435,3 +438,39 @@ class AuthorizedTests(ApiTest): self.assertEqual( self.signed('/api/epub/grandchild/').content, "") + + def test_publish(self): + response = self.signed('/api/books/', + method='POST', + data={"data": json.dumps({})}) + self.assertEqual(response.status_code, 403) + + response = self.signed('/api/pictures/', + method='POST', + data={"data": json.dumps({})}) + self.assertEqual(response.status_code, 403) + + self.user.is_superuser = True + self.user.save() + + with patch('catalogue.models.Book.from_xml_file') as mock: + response = self.signed('/api/books/', + method='POST', + data={"data": json.dumps({ + "book_xml": "" + })}) + self.assertTrue(mock.called) + self.assertEqual(response.status_code, 201) + + with patch('picture.models.Picture.from_xml_file') as mock: + response = self.signed('/api/pictures/', + method='POST', + data={"data": json.dumps({ + "picture_xml": "", + "picture_image_data": "Kg==", + })}) + self.assertTrue(mock.called) + self.assertEqual(response.status_code, 201) + + self.user.is_superuser = False + self.user.save() diff --git a/src/api/urls.py b/src/api/urls.py index d93bcce00..973bf030a 100644 --- a/src/api/urls.py +++ b/src/api/urls.py @@ -5,53 +5,12 @@ from django.conf.urls import url, include from django.views.decorators.csrf import csrf_exempt from django.views.generic import TemplateView -from piston.authentication import OAuthAuthentication, oauth_access_token, oauth_request_token -from piston.resource import Resource +from piston.authentication import oauth_access_token, oauth_request_token import catalogue.views from api import handlers -from api.helpers import CsrfExemptResource from api.piston_patch import oauth_user_auth from . import views -auth = OAuthAuthentication(realm="Wolne Lektury") - - -class DjangoAuthentication(object): - """ - Authentication handler that always returns - True, so no authentication is needed, nor - initiated (`challenge` is missing.) - """ - def is_authenticated(self, request): - return request.user.is_authenticated() - - def challenge(self): - from django.http import HttpResponse - resp = HttpResponse("Authorization Required") - resp.status_code = 401 - return resp - - -def auth_resource(handler): - from django.conf import settings - if settings.DEBUG: - django_auth = DjangoAuthentication() - return CsrfExemptResource(handler=handler, authentication=django_auth) - return CsrfExemptResource(handler=handler, authentication=auth) - - -book_list_resource = auth_resource(handler=handlers.BooksHandler) -ebook_list_resource = Resource(handler=handlers.EBooksHandler) -filter_book_resource = auth_resource(handler=handlers.FilterBooksHandler) - -picture_resource = auth_resource(handler=handlers.PictureHandler) - -blog_resource = Resource(handler=handlers.BlogEntryHandler) - - -tags_re = r'^(?P(?:(?:[a-z0-9-]+/){2}){0,6})' -paginate_re = r'(?:after/(?P[a-z0-9-]+)/)?(?:count/(?P[0-9]+)/)?$' - urlpatterns = [ url(r'^oauth/request_token/$', oauth_request_token), @@ -69,18 +28,9 @@ urlpatterns = [ url(r'^reading/(?P[a-z0-9-]+)/(?P[a-z]+)/$', views.BookUserDataView.as_view(), name='api_reading'), url(r'^username/$', views.UserView.as_view(), name='api_username'), - # books by tags - url(tags_re + r'ebooks/' + paginate_re, - ebook_list_resource, name='api_ebook_list'), - url(tags_re + r'parent_ebooks/' + paginate_re, - ebook_list_resource, {"top_level": True}, name='api_parent_ebook_list'), - - url(r'^filter-books/$', filter_book_resource, name='api_filter_books'), - - url(r'^pictures/$', picture_resource), - - url(r'^blog/$', blog_resource), + url(r'^blog/$', views.BlogView.as_view()), + url(r'^pictures/$', include('picture.api.urls')), url(r'^', include('social.api.urls')), url(r'^', include('catalogue.api.urls')), ] diff --git a/src/api/views.py b/src/api/views.py index 812be832c..377beb681 100644 --- a/src/api/views.py +++ b/src/api/views.py @@ -1,8 +1,13 @@ +# -*- 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 django.http import Http404 from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework.views import APIView -from rest_framework.generics import RetrieveAPIView, get_object_or_404 +from rest_framework.generics import ListAPIView, RetrieveAPIView, get_object_or_404 +from migdal.models import Entry from catalogue.models import Book from .models import BookUserData from . import serializers @@ -39,3 +44,17 @@ class BookUserDataView(RetrieveAPIView): instance = BookUserData.update(book, request.user, state) serializer = self.get_serializer(instance) return Response(serializer.data) + + +class BlogView(ListAPIView): + serializer_class = serializers.BlogSerializer + + def get_queryset(self): + after = self.request.query_params.get('after') + count = int(self.request.query_params.get('count', 20)) + entries = Entry.published_objects.filter(in_stream=True).order_by('-first_published_at') + if after: + entries = entries.filter(first_published_at__lt=after) + if count: + entries = entries[:count] + return entries diff --git a/src/catalogue/api/fields.py b/src/catalogue/api/fields.py index 145780e2b..cab947592 100644 --- a/src/catalogue/api/fields.py +++ b/src/catalogue/api/fields.py @@ -1,5 +1,8 @@ +# -*- 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 rest_framework import serializers -from sorl.thumbnail import default from catalogue.models import Book @@ -16,15 +19,3 @@ class BookLiked(serializers.ReadOnlyField): request.liked_books = None if request.liked_books is not None: return value in request.liked_books - - -class ThumbnailField(serializers.FileField): - def __init__(self, geometry, *args, **kwargs): - self.geometry = geometry - super(ThumbnailField, self).__init__(*args, **kwargs) - - def to_representation(self, value): - if value: - return super(ThumbnailField, self).to_representation( - default.backend.get_thumbnail(value, self.geometry) - ) diff --git a/src/catalogue/api/serializers.py b/src/catalogue/api/serializers.py index ea48cdba4..a5069e3bd 100644 --- a/src/catalogue/api/serializers.py +++ b/src/catalogue/api/serializers.py @@ -1,7 +1,11 @@ +# -*- 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 rest_framework import serializers -from api.fields import AbsoluteURLField, LegacyMixin +from api.fields import AbsoluteURLField, LegacyMixin, ThumbnailField from catalogue.models import Book, Collection, Tag, BookMedia, Fragment -from .fields import BookLiked, ThumbnailField +from .fields import BookLiked class TagSerializer(serializers.ModelSerializer): @@ -34,7 +38,6 @@ class BookSerializer(LegacyMixin, serializers.ModelSerializer): simple_thumb = serializers.FileField(source='cover_api_thumb') href = AbsoluteURLField(view_name='catalogue_api_book', view_args=['slug']) url = AbsoluteURLField() - cover = serializers.FileField() cover_thumb = ThumbnailField('139x193', source='cover') class Meta: @@ -55,6 +58,15 @@ class BookListSerializer(BookSerializer): Meta = BookSerializer.Meta +class FilterBookListSerializer(BookListSerializer): + key = serializers.CharField() + + class Meta: + model = Book + fields = BookListSerializer.Meta.fields + ['key'] + legacy_non_null_fields = BookListSerializer.Meta.legacy_non_null_fields + + class MediaSerializer(LegacyMixin, serializers.ModelSerializer): url = serializers.FileField(source='file') @@ -107,7 +119,13 @@ class BookPreviewSerializer(BookDetailSerializer): legacy_non_null_fields = BookDetailSerializer.Meta.legacy_non_null_fields -class EbookSerializer(BookSerializer): +class EbookSerializer(BookListSerializer): + txt = AbsoluteURLField(source='txt_url') + fb2 = AbsoluteURLField(source='fb2_url') + epub = AbsoluteURLField(source='epub_url') + mobi = AbsoluteURLField(source='mobi_url') + pdf = AbsoluteURLField(source='pdf_url') + class Meta: model = Book fields = ['author', 'href', 'title', 'cover', 'slug'] + Book.ebook_formats diff --git a/src/catalogue/api/urls.py b/src/catalogue/api/urls.py index 13cebead5..07729aea8 100644 --- a/src/catalogue/api/urls.py +++ b/src/catalogue/api/urls.py @@ -33,6 +33,16 @@ urlpatterns = [ url(r'^books/(?P[^/]+)/$', views.BookDetail.as_view(), name='catalogue_api_book'), + url(tags_re + r'ebooks/' + paginate_re, + views.EbookList.as_view(), + name='catalogue_api_ebook_list'), + url(tags_re + r'parent_ebooks/' + paginate_re, + views.EbookList.as_view(), + {"top_level": True}, + name='catalogue_api_parent_ebook_list'), + + url(r'^filter-books/$', views.FilterBookList.as_view(), name='catalogue_api_filter_books'), + url(r'^epub/(?P[a-z0-9-]+)/$', views.EpubView.as_view(), name='catalogue_api_epub'), url(r'^preview/$', views.Preview.as_view(), name='catalogue_api_preview'), diff --git a/src/catalogue/api/views.py b/src/catalogue/api/views.py index f3b719309..ee345ba68 100644 --- a/src/catalogue/api/views.py +++ b/src/catalogue/api/views.py @@ -1,13 +1,24 @@ +# -*- 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. +# +import json from django.http import Http404, HttpResponse from rest_framework.generics import ListAPIView, RetrieveAPIView, get_object_or_404 from rest_framework.permissions import DjangoModelPermissionsOrAnonReadOnly +from rest_framework.response import Response from rest_framework import status from paypal.permissions import IsSubscribed from api.handlers import read_tags from .helpers import books_after, order_books from . import serializers +from catalogue.forms import BookImportForm from catalogue.models import Book, Collection, Tag, Fragment from catalogue.models.tag import prefetch_relations +from wolnelektury.utils import re_escape + + +book_tag_categories = ['author', 'epoch', 'kind', 'genre'] class CollectionList(ListAPIView): @@ -75,7 +86,7 @@ class BookList(ListAPIView): return books - def post(self, request): + def post(self, request, **kwargs): # Permission needed. data = json.loads(request.POST.get('data')) form = BookImportForm(data) @@ -92,11 +103,86 @@ class BookDetail(RetrieveAPIView): serializer_class = serializers.BookDetailSerializer +class EbookList(BookList): + serializer_class = serializers.EbookSerializer + + class Preview(ListAPIView): queryset = Book.objects.filter(preview=True) serializer_class = serializers.BookPreviewSerializer +class FilterBookList(ListAPIView): + serializer_class = serializers.FilterBookListSerializer + + def parse_bool(self, s): + if s in ('true', 'false'): + return s == 'true' + else: + return None + + def get_queryset(self): + key_sep = '$' + search_string = self.request.query_params.get('search') + is_lektura = self.parse_bool(self.request.query_params.get('lektura')) + is_audiobook = self.parse_bool(self.request.query_params.get('audiobook')) + preview = self.parse_bool(self.request.query_params.get('preview')) + + new_api = self.request.query_params.get('new_api') + after = self.request.query_params.get('after') + count = int(self.request.query_params.get('count', 50)) + books = order_books(Book.objects.distinct(), new_api) + 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') + if preview is not None: + books = books.filter(preview=preview) + for category in book_tag_categories: + category_plural = category + 's' + if category_plural in self.request.query_params: + slugs = self.request.query_params[category_plural].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: + search_string = re_escape(search_string) + books_author = books.filter(cached_author__iregex=r'\m' + search_string) + books_title = books.filter(title__iregex=r'\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, key = after.split(key_sep, 1) + if which == 'title': + book_lists = [(books_after(books_title, key, new_api), 'title')] + else: # which == 'author' + book_lists = [(books_after(books_author, key, new_api), 'author'), (books_title, 'title')] + else: + book_lists = [(books_author, 'author'), (books_title, 'title')] + else: + if after and key_sep in after: + which, key = after.split(key_sep, 1) + books = books_after(books, key, new_api) + book_lists = [(books, 'book')] + + filtered_books = [] + for book_list, label in book_lists: + for category in book_tag_categories: + book_list = prefetch_relations(book_list, category) + remaining_count = count - len(filtered_books) + for book in book_list[:remaining_count]: + book.key = '%s%s%s' % ( + label, key_sep, book.slug if not new_api else book.full_sort_key()) + filtered_books.append(book) + if len(filtered_books) == count: + break + + return filtered_books + + class EpubView(RetrieveAPIView): queryset = Book.objects.all() lookup_field = 'slug' diff --git a/src/picture/api/__init__.py b/src/picture/api/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/picture/api/urls.py b/src/picture/api/urls.py new file mode 100644 index 000000000..e8dc14618 --- /dev/null +++ b/src/picture/api/urls.py @@ -0,0 +1,11 @@ +# -*- 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 django.conf.urls import url +from . import views + + +urlpatterns = [ + url(r'^$', views.PicturesView.as_view()), +] diff --git a/src/picture/api/views.py b/src/picture/api/views.py new file mode 100644 index 000000000..fddc86c17 --- /dev/null +++ b/src/picture/api/views.py @@ -0,0 +1,26 @@ +# -*- 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. +# +import json +from django.http import Http404 +from rest_framework.permissions import DjangoModelPermissions +from rest_framework.response import Response +from rest_framework import status +from rest_framework.views import APIView +from picture.forms import PictureImportForm +from picture.models import Picture + + +class PicturesView(APIView): + permission_classes = [DjangoModelPermissions] + queryset = Picture.objects.none() # Required for DjangoModelPermissions + + def post(self, request): + data = json.loads(request.POST.get('data')) + form = PictureImportForm(data) + if form.is_valid(): + form.save() + return Response({}, status=status.HTTP_201_CREATED) + else: + raise Http404 -- 2.20.1 From ea221b2bc448ce22dfda4f1ead2968fa7de31771 Mon Sep 17 00:00:00 2001 From: Radek Czajka Date: Thu, 7 Feb 2019 00:56:21 +0100 Subject: [PATCH 03/16] Start replacing Piston in OAuth flow with OAuthLib. --- requirements/requirements.txt | 1 + src/api/drf_auth.py | 29 +++++++----- src/api/request_validator.py | 85 +++++++++++++++++++++++++++++++++++ src/api/tests/tests.py | 27 ++++++----- src/api/urls.py | 2 +- src/api/views.py | 41 ++++++++++++++++- 6 files changed, 159 insertions(+), 26 deletions(-) create mode 100644 src/api/request_validator.py diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 3b8c95e42..34f4d9a38 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -15,6 +15,7 @@ django-allauth>=0.32,<0.33 django-extensions djangorestframework<3.7 djangorestframework-xml +oauthlib>=3.0.1,<3.1 # contact pyyaml diff --git a/src/api/drf_auth.py b/src/api/drf_auth.py index 26018c61e..ca6a49188 100644 --- a/src/api/drf_auth.py +++ b/src/api/drf_auth.py @@ -1,20 +1,29 @@ -""" -Transitional code: bridge between Piston's OAuth implementation -and DRF views. -""" -from piston.authentication import OAuthAuthentication +# -*- 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 oauthlib.oauth1 import ResourceEndpoint from rest_framework.authentication import BaseAuthentication +from .request_validator import PistonRequestValidator class PistonOAuthAuthentication(BaseAuthentication): def __init__(self): - self.piston_auth = OAuthAuthentication() + validator = PistonRequestValidator() + self.provider = ResourceEndpoint(validator) def authenticate_header(self, request): return 'OAuth realm="API"' def authenticate(self, request): - if self.piston_auth.is_valid_request(request): - consumer, token, parameters = self.piston_auth.validate_token(request) - if consumer and token: - return token.user, token + v, r = self.provider.validate_protected_resource_request( + request.build_absolute_uri(), + http_method=request.method, + body=request.body, + headers={ + "Authorization": request.META['HTTP_AUTHORIZATION'], + "Content-Type": request.content_type, + } if 'HTTP_AUTHORIZATION' in request.META else None + ) + if v: + return r.token.user, r.token diff --git a/src/api/request_validator.py b/src/api/request_validator.py new file mode 100644 index 000000000..b8554adef --- /dev/null +++ b/src/api/request_validator.py @@ -0,0 +1,85 @@ +# -*- 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 oauthlib.oauth1 import RequestValidator +from piston.models import Consumer, Nonce, Token + + +class PistonRequestValidator(RequestValidator): + dummy_access_token = '!' + realms = ['API'] + + # Just for the tests. + # It'd be a little more kosher to use test client with secure=True. + enforce_ssl = False + + # iOS app generates 8-char nonces. + nonce_length = 8, 250 + + # Because piston.models.Token.key is char(18). + access_token_length = 18, 32 + + def check_client_key(self, client_key): + """We control the keys anyway.""" + return True + + def get_access_token_secret(self, client_key, token, request): + return request.token.secret + + def get_default_realms(self, client_key, request): + return ['API'] + + def validate_access_token(self, client_key, token, request): + try: + token = Token.objects.get( + token_type=Token.ACCESS, + consumer__key=client_key, + key=token + ) + except Token.DoesNotExist: + return False + else: + request.token = token + return True + + def validate_timestamp_and_nonce(self, client_key, timestamp, nonce, + request, request_token=None, access_token=None): + # TODO: validate the timestamp + token = request_token or access_token + # Yes, this is what Piston did. + if token is None: + return True + + nonce, created = Nonce.objects.get_or_create(consumer_key=client_key, + token_key=token, + key=nonce) + return created + + def validate_client_key(self, client_key, request): + try: + request.oauth_consumer = Consumer.objects.get(key=client_key) + except Consumer.DoesNotExist: + return False + return True + + def validate_realms(self, client_key, token, request, uri=None, realms=None): + return True + + def validate_requested_realms(self, *args, **kwargs): + return True + + def validate_redirect_uri(self, *args, **kwargs): + return True + + def get_client_secret(self, client_key, request): + return request.oauth_consumer.secret + + def save_request_token(self, token, request): + Token.objects.create( + token_type=Token.REQUEST, + timestamp=request.timestamp, + key=token['oauth_token'], + secret=token['oauth_token_secret'], + consumer=request.oauth_consumer, + ) diff --git a/src/api/tests/tests.py b/src/api/tests/tests.py index 38f1882bb..ee3d66e6f 100644 --- a/src/api/tests/tests.py +++ b/src/api/tests/tests.py @@ -253,7 +253,7 @@ class OAuth1Tests(ApiTest): User.objects.all().delete() def test_create_token(self): - base_query = ("oauth_consumer_key=client&oauth_nonce=123&" + base_query = ("oauth_consumer_key=client&oauth_nonce=12345678&" "oauth_signature_method=HMAC-SHA1&oauth_timestamp={}&" "oauth_version=1.0".format(int(time()))) raw = '&'.join([ @@ -274,7 +274,7 @@ class OAuth1Tests(ApiTest): key=request_token['oauth_token'][0], token_type=Token.REQUEST ).update(user=self.user, is_approved=True) - base_query = ("oauth_consumer_key=client&oauth_nonce=123&" + base_query = ("oauth_consumer_key=client&oauth_nonce=12345678&" "oauth_signature_method=HMAC-SHA1&oauth_timestamp={}&" "oauth_token={}&oauth_version=1.0".format( int(time()), request_token['oauth_token'][0])) @@ -330,7 +330,7 @@ class AuthorizedTests(ApiTest): def signed(self, url, method='GET', params=None, data=None): auth_params = { "oauth_consumer_key": self.consumer.key, - "oauth_nonce": "%f" % time(), + "oauth_nonce": ("%f" % time()).replace('.', ''), "oauth_signature_method": "HMAC-SHA1", "oauth_timestamp": int(time()), "oauth_token": self.token.key, @@ -358,10 +358,11 @@ class AuthorizedTests(ApiTest): if params: url = url + '?' + urlencode(params) return getattr(self.client, method.lower())( - url, - data=data, - HTTP_AUTHORIZATION=auth - ) + url, + data=urlencode(data) if data else None, + content_type='application/x-www-form-urlencoded', + HTTP_AUTHORIZATION=auth, + ) def signed_json(self, url, method='GET', params=None, data=None): return json.loads(self.signed(url, method, params, data).content) @@ -371,10 +372,9 @@ class AuthorizedTests(ApiTest): [b['liked'] for b in self.signed_json('/api/books/')], [False, False, False] ) - # This one fails in the legacy implementation - # data = self.signed_json('/api/books/child/') - # self.assertFalse(data['parent']['liked']) - # self.assertFalse(data['children'][0]['liked']) + data = self.signed_json('/api/books/child/') + self.assertFalse(data['parent']['liked']) + self.assertFalse(data['children'][0]['liked']) self.assertEqual( self.signed_json('/api/like/parent/'), @@ -390,9 +390,8 @@ class AuthorizedTests(ApiTest): self.assertTrue(self.signed_json( '/api/filter-books/', params={"search": "parent"})[0]['liked']) - # This one fails in the legacy implementation. - #self.assertTrue(self.signed_json( - # '/api/books/child/')['parent']['liked']) + self.assertTrue(self.signed_json( + '/api/books/child/')['parent']['liked']) # Liked books go on shelf. self.assertEqual( [x['slug'] for x in self.signed_json('/api/shelf/likes/')], diff --git a/src/api/urls.py b/src/api/urls.py index 973bf030a..150dc4c0b 100644 --- a/src/api/urls.py +++ b/src/api/urls.py @@ -13,7 +13,7 @@ from . import views urlpatterns = [ - url(r'^oauth/request_token/$', oauth_request_token), + url(r'^oauth/request_token/$', views.OAuth1RequestTokenView.as_view()), url(r'^oauth/authorize/$', oauth_user_auth, name='oauth_user_auth'), url(r'^oauth/access_token/$', csrf_exempt(oauth_access_token)), diff --git a/src/api/views.py b/src/api/views.py index 377beb681..6d462fa3a 100644 --- a/src/api/views.py +++ b/src/api/views.py @@ -2,7 +2,10 @@ # This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later. # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information. # -from django.http import Http404 +from django.http import Http404, HttpResponse +from oauthlib.common import urlencode +from oauthlib.oauth1 import RequestTokenEndpoint +from piston.models import KEY_SIZE, SECRET_SIZE from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework.views import APIView @@ -11,6 +14,42 @@ from migdal.models import Entry from catalogue.models import Book from .models import BookUserData from . import serializers +from .request_validator import PistonRequestValidator + + +class OAuth1RequestTokenEndpoint(RequestTokenEndpoint): + def _create_request(self, *args, **kwargs): + r = super(OAuth1RequestTokenEndpoint, self)._create_request(*args, **kwargs) + r.redirect_uri = 'oob' + return r + + def create_request_token(self, request, credentials): + token = { + 'oauth_token': self.token_generator()[:KEY_SIZE], + 'oauth_token_secret': self.token_generator()[:SECRET_SIZE], + } + token.update(credentials) + self.request_validator.save_request_token(token, request) + return urlencode(token.items()) + + +class OAuth1RequestTokenView(APIView): + def __init__(self): + self.endpoint = OAuth1RequestTokenEndpoint(PistonRequestValidator()) + def dispatch(self, request): + headers, body, status = self.endpoint.create_request_token_response( + request.build_absolute_uri(), + request.method, + request.body, + { + "Authorization": request.META['HTTP_AUTHORIZATION'] + } if 'HTTP_AUTHORIZATION' in request.META else None + ) + + response = HttpResponse(body, status=status) + for k, v in headers.items(): + response[k] = v + return response class UserView(RetrieveAPIView): -- 2.20.1 From 1434b3dfc0495e885921acb726df6cc06e00c5ed Mon Sep 17 00:00:00 2001 From: Radek Czajka Date: Fri, 8 Feb 2019 01:30:34 +0100 Subject: [PATCH 04/16] Move authorize endpoint to OAuthlib. --- src/api/drf_auth.py | 9 +-- src/api/piston_patch.py | 125 ++++++++--------------------------- src/api/request_validator.py | 27 +++++++- src/api/urls.py | 2 +- src/api/utils.py | 33 +++++++++ src/api/views.py | 20 ++---- 6 files changed, 98 insertions(+), 118 deletions(-) create mode 100644 src/api/utils.py diff --git a/src/api/drf_auth.py b/src/api/drf_auth.py index ca6a49188..893f06610 100644 --- a/src/api/drf_auth.py +++ b/src/api/drf_auth.py @@ -5,6 +5,7 @@ from oauthlib.oauth1 import ResourceEndpoint from rest_framework.authentication import BaseAuthentication from .request_validator import PistonRequestValidator +from .utils import oauthlib_request class PistonOAuthAuthentication(BaseAuthentication): @@ -17,13 +18,7 @@ class PistonOAuthAuthentication(BaseAuthentication): def authenticate(self, request): v, r = self.provider.validate_protected_resource_request( - request.build_absolute_uri(), - http_method=request.method, - body=request.body, - headers={ - "Authorization": request.META['HTTP_AUTHORIZATION'], - "Content-Type": request.content_type, - } if 'HTTP_AUTHORIZATION' in request.META else None + **oauthlib_request(request) ) if v: return r.token.user, r.token diff --git a/src/api/piston_patch.py b/src/api/piston_patch.py index fb3987bde..3c7e50f4f 100644 --- a/src/api/piston_patch.py +++ b/src/api/piston_patch.py @@ -1,18 +1,14 @@ # -*- coding: utf-8 -*- - -# modified from django-piston -import base64 -import hmac - -from django import forms -from django.conf import settings +# This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later. +# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information. +# +from oauthlib.oauth1 import AuthorizationEndpoint from django.contrib.auth.decorators import login_required -from django.core.urlresolvers import get_callable -from django.http import HttpResponseRedirect, HttpResponse -from django.shortcuts import render_to_response -from django.template.context import RequestContext -from piston import oauth -from piston.authentication import initialize_server_request, INVALID_PARAMS_RESPONSE, send_oauth_error +from django import forms +from django.http import HttpResponseRedirect +from django.shortcuts import render +from .request_validator import PistonRequestValidator +from .utils import oauthlib_request, oauthlib_response class HttpResponseAppRedirect(HttpResponseRedirect): @@ -23,95 +19,32 @@ class OAuthAuthenticationForm(forms.Form): oauth_token = forms.CharField(widget=forms.HiddenInput) oauth_callback = forms.CharField(widget=forms.HiddenInput) # changed from URLField - too strict # removed authorize_access - redundant - csrf_signature = forms.CharField(widget=forms.HiddenInput) - - def __init__(self, *args, **kwargs): - forms.Form.__init__(self, *args, **kwargs) - - self.fields['csrf_signature'].initial = self.initial_csrf_signature - - def clean_csrf_signature(self): - sig = self.cleaned_data['csrf_signature'] - token = self.cleaned_data['oauth_token'] - - sig1 = OAuthAuthenticationForm.get_csrf_signature(settings.SECRET_KEY, token) - - if sig != sig1: - raise forms.ValidationError("CSRF signature is not valid") - - return sig - - def initial_csrf_signature(self): - token = self.initial['oauth_token'] - return OAuthAuthenticationForm.get_csrf_signature(settings.SECRET_KEY, token) - - @staticmethod - def get_csrf_signature(key, token): - # Check signature... - import hashlib # 2.5 - hashed = hmac.new(key, token, hashlib.sha1) - - # calculate the digest base 64 - return base64.b64encode(hashed.digest()) - - -# The only thing changed in the views below is the form used -# and also the Http Redirect class - - -def oauth_auth_view(request, token, callback, params): - form = OAuthAuthenticationForm(initial={ - 'oauth_token': token.key, - 'oauth_callback': callback, - }) - - return render_to_response('piston/authorize_token.html', - {'form': form}, RequestContext(request)) @login_required def oauth_user_auth(request): - oauth_server, oauth_request = initialize_server_request(request) - - if oauth_request is None: - return INVALID_PARAMS_RESPONSE - - try: - token = oauth_server.fetch_request_token(oauth_request) - except oauth.OAuthError, err: - return send_oauth_error(err) - - try: - callback = oauth_server.get_callback(oauth_request) - except: - callback = None + endpoint = AuthorizationEndpoint(PistonRequestValidator()) if request.method == "GET": - params = oauth_request.get_normalized_parameters() - - oauth_view = getattr(settings, 'OAUTH_AUTH_VIEW', None) - if oauth_view is None: - return oauth_auth_view(request, token, callback, params) - else: - return get_callable(oauth_view)(request, token, callback, params) - elif request.method == "POST": - try: - form = OAuthAuthenticationForm(request.POST) - if form.is_valid(): - token = oauth_server.authorize_token(token, request.user) - args = '?' + token.to_string(only_key=True) - else: - args = '?error=%s' % 'Access not granted by user.' - - if not callback: - callback = getattr(settings, 'OAUTH_CALLBACK_VIEW') - return get_callable(callback)(request, token) + # Why not just get oauth_token here? + # This is fairly straightforward, in't? + realms, credentials = endpoint.get_realms_and_credentials( + **oauthlib_request(request)) + callback = request.GET.get('oauth_callback') - response = HttpResponseAppRedirect(callback + args) + form = OAuthAuthenticationForm(initial={ + 'oauth_token': credentials['resource_owner_key'], + 'oauth_callback': callback, + }) - except oauth.OAuthError, err: - response = send_oauth_error(err) - else: - response = HttpResponse('Action not allowed.') + return render(request, 'piston/authorize_token.html', {'form': form}) - return response + elif request.method == "POST": + response = oauthlib_response( + endpoint.create_authorization_response( + credentials={"user": request.user}, + **oauthlib_request(request) + ) + ) + + return response diff --git a/src/api/request_validator.py b/src/api/request_validator.py index b8554adef..6e3c0c22b 100644 --- a/src/api/request_validator.py +++ b/src/api/request_validator.py @@ -2,11 +2,14 @@ # This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later. # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information. # +import time from oauthlib.oauth1 import RequestValidator from piston.models import Consumer, Nonce, Token class PistonRequestValidator(RequestValidator): + timestamp_threshold = 300 + dummy_access_token = '!' realms = ['API'] @@ -45,7 +48,8 @@ class PistonRequestValidator(RequestValidator): def validate_timestamp_and_nonce(self, client_key, timestamp, nonce, request, request_token=None, access_token=None): - # TODO: validate the timestamp + if abs(time.time() - int(timestamp)) > self.timestamp_threshold: + return False token = request_token or access_token # Yes, this is what Piston did. if token is None: @@ -83,3 +87,24 @@ class PistonRequestValidator(RequestValidator): secret=token['oauth_token_secret'], consumer=request.oauth_consumer, ) + + def verify_request_token(self, token, request): + return Token.objects.filter( + token_type=Token.REQUEST, key=token, is_approved=False + ).exists() + + def get_realms(self, *args, **kwargs): + return [] + + def save_verifier(self, token, verifier, request): + Token.objects.filter( + token_type=Token.REQUEST, + key=token, + is_approved=False + ).update( + is_approved=True, + user=verifier['user'] + ) + + def get_redirect_uri(self, token, request): + return request.redirect_uri diff --git a/src/api/urls.py b/src/api/urls.py index 150dc4c0b..5936e1b64 100644 --- a/src/api/urls.py +++ b/src/api/urls.py @@ -5,7 +5,7 @@ from django.conf.urls import url, include from django.views.decorators.csrf import csrf_exempt from django.views.generic import TemplateView -from piston.authentication import oauth_access_token, oauth_request_token +from piston.authentication import oauth_access_token import catalogue.views from api import handlers from api.piston_patch import oauth_user_auth diff --git a/src/api/utils.py b/src/api/utils.py new file mode 100644 index 000000000..6dc7e4505 --- /dev/null +++ b/src/api/utils.py @@ -0,0 +1,33 @@ +# -*- 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 django.http import HttpResponse +from django.utils.encoding import iri_to_uri + + +def oauthlib_request(request): + """Creates parameters for OAuthlib's Request from a Django Request.""" + headers = {} + # We don't have request.content_type yet in 2015, + # while test client has no META['CONTENT_TYPE']. + ct = request.META.get('CONTENT_TYPE', getattr(request, 'content_type', None)) + if ct: + headers["Content-Type"] = ct + if 'HTTP_AUTHORIZATION' in request.META: + headers["Authorization"] = request.META['HTTP_AUTHORIZATION'] + return { + "uri": request.build_absolute_uri(), + "http_method": request.method, + "body": request.body, + "headers": headers, + } + +def oauthlib_response((headers, body, status)): + """Creates a django.http.HttpResponse from (headers, body, status) tuple from OAuthlib.""" + response = HttpResponse(body, status=status) + for k, v in headers.items(): + if k == 'Location': + v = iri_to_uri(v) + response[k] = v + return response diff --git a/src/api/views.py b/src/api/views.py index 6d462fa3a..3cf957ed9 100644 --- a/src/api/views.py +++ b/src/api/views.py @@ -2,7 +2,7 @@ # This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later. # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information. # -from django.http import Http404, HttpResponse +from django.http import Http404 from oauthlib.common import urlencode from oauthlib.oauth1 import RequestTokenEndpoint from piston.models import KEY_SIZE, SECRET_SIZE @@ -15,6 +15,7 @@ from catalogue.models import Book from .models import BookUserData from . import serializers from .request_validator import PistonRequestValidator +from .utils import oauthlib_request, oauthlib_response class OAuth1RequestTokenEndpoint(RequestTokenEndpoint): @@ -36,21 +37,14 @@ class OAuth1RequestTokenEndpoint(RequestTokenEndpoint): class OAuth1RequestTokenView(APIView): def __init__(self): self.endpoint = OAuth1RequestTokenEndpoint(PistonRequestValidator()) + def dispatch(self, request): - headers, body, status = self.endpoint.create_request_token_response( - request.build_absolute_uri(), - request.method, - request.body, - { - "Authorization": request.META['HTTP_AUTHORIZATION'] - } if 'HTTP_AUTHORIZATION' in request.META else None + return oauthlib_response( + self.endpoint.create_request_token_response( + **oauthlib_request(request) + ) ) - response = HttpResponse(body, status=status) - for k, v in headers.items(): - response[k] = v - return response - class UserView(RetrieveAPIView): permission_classes = [IsAuthenticated] -- 2.20.1 From 29f694eea94d09bd66976321ddfd80ac30b73d38 Mon Sep 17 00:00:00 2001 From: Radek Czajka Date: Fri, 8 Feb 2019 11:18:58 +0100 Subject: [PATCH 05/16] Last Piston view replaced using OAuthlib. --- src/api/request_validator.py | 39 ++++++++++++++++++++++++++++++++++++ src/api/tests/tests.py | 2 +- src/api/urls.py | 3 +-- src/api/views.py | 36 +++++++++++++++++++++++++++++++-- 4 files changed, 75 insertions(+), 5 deletions(-) diff --git a/src/api/request_validator.py b/src/api/request_validator.py index 6e3c0c22b..916636b91 100644 --- a/src/api/request_validator.py +++ b/src/api/request_validator.py @@ -21,18 +21,37 @@ class PistonRequestValidator(RequestValidator): nonce_length = 8, 250 # Because piston.models.Token.key is char(18). + request_token_length = 18, 32 access_token_length = 18, 32 + # TODO: oauthlib request-access switch. def check_client_key(self, client_key): """We control the keys anyway.""" return True + def get_request_token_secret(self, client_key, token, request): + return request.token.secret + def get_access_token_secret(self, client_key, token, request): return request.token.secret def get_default_realms(self, client_key, request): return ['API'] + def validate_request_token(self, client_key, token, request): + try: + token = Token.objects.get( + token_type=Token.REQUEST, + consumer__key=client_key, + key=token, + is_approved=True, + ) + except Token.DoesNotExist: + return False + else: + request.token = token + return True + def validate_access_token(self, client_key, token, request): try: token = Token.objects.get( @@ -76,6 +95,9 @@ class PistonRequestValidator(RequestValidator): def validate_redirect_uri(self, *args, **kwargs): return True + def validate_verifier(self, client_key, token, verifier, request): + return True + def get_client_secret(self, client_key, request): return request.oauth_consumer.secret @@ -88,6 +110,16 @@ class PistonRequestValidator(RequestValidator): consumer=request.oauth_consumer, ) + def save_access_token(self, token, request): + Token.objects.create( + token_type=Token.ACCESS, + timestamp=request.timestamp, + key=token['oauth_token'], + secret=token['oauth_token_secret'], + consumer=request.oauth_consumer, + user=request.token.user, + ) + def verify_request_token(self, token, request): return Token.objects.filter( token_type=Token.REQUEST, key=token, is_approved=False @@ -108,3 +140,10 @@ class PistonRequestValidator(RequestValidator): def get_redirect_uri(self, token, request): return request.redirect_uri + + def invalidate_request_token(self, client_key, request_token, request): + Token.objects.filter( + token_type=Token.REQUEST, + key=request_token, + consumer__key=client_key, + ) diff --git a/src/api/tests/tests.py b/src/api/tests/tests.py index ee3d66e6f..c8e07d26c 100644 --- a/src/api/tests/tests.py +++ b/src/api/tests/tests.py @@ -45,7 +45,7 @@ class ApiTest(TestCase): with open(filename) as f: good_content = f.read().rstrip() self.assertEqual(content, good_content, content) - + def assert_json_response(self, url, name): data = self.load_json(url) filename = path.join(path.dirname(__file__), 'res', 'responses', name) diff --git a/src/api/urls.py b/src/api/urls.py index 5936e1b64..8f9914113 100644 --- a/src/api/urls.py +++ b/src/api/urls.py @@ -5,7 +5,6 @@ from django.conf.urls import url, include from django.views.decorators.csrf import csrf_exempt from django.views.generic import TemplateView -from piston.authentication import oauth_access_token import catalogue.views from api import handlers from api.piston_patch import oauth_user_auth @@ -15,7 +14,7 @@ from . import views urlpatterns = [ url(r'^oauth/request_token/$', views.OAuth1RequestTokenView.as_view()), url(r'^oauth/authorize/$', oauth_user_auth, name='oauth_user_auth'), - url(r'^oauth/access_token/$', csrf_exempt(oauth_access_token)), + url(r'^oauth/access_token/$', csrf_exempt(views.OAuth1AccessTokenView.as_view())), url(r'^$', TemplateView.as_view(template_name='api/main.html'), name='api'), diff --git a/src/api/views.py b/src/api/views.py index 3cf957ed9..518445dd0 100644 --- a/src/api/views.py +++ b/src/api/views.py @@ -3,8 +3,9 @@ # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information. # from django.http import Http404 +from django.views.generic.base import View from oauthlib.common import urlencode -from oauthlib.oauth1 import RequestTokenEndpoint +from oauthlib.oauth1 import RequestTokenEndpoint, AccessTokenEndpoint from piston.models import KEY_SIZE, SECRET_SIZE from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response @@ -34,7 +35,7 @@ class OAuth1RequestTokenEndpoint(RequestTokenEndpoint): return urlencode(token.items()) -class OAuth1RequestTokenView(APIView): +class OAuth1RequestTokenView(View): def __init__(self): self.endpoint = OAuth1RequestTokenEndpoint(PistonRequestValidator()) @@ -46,6 +47,37 @@ class OAuth1RequestTokenView(APIView): ) +class OAuth1AccessTokenEndpoint(AccessTokenEndpoint): + def _create_request(self, *args, **kwargs): + r = super(OAuth1AccessTokenEndpoint, self)._create_request(*args, **kwargs) + r.verifier = 'x' * 20 + return r + + def create_access_token(self, request, credentials): + request.realms = self.request_validator.get_realms( + request.resource_owner_key, request) + token = { + 'oauth_token': self.token_generator()[:KEY_SIZE], + 'oauth_token_secret': self.token_generator()[:SECRET_SIZE], + 'oauth_authorized_realms': ' '.join(request.realms) + } + token.update(credentials) + self.request_validator.save_access_token(token, request) + return urlencode(token.items()) + + +class OAuth1AccessTokenView(View): + def __init__(self): + self.endpoint = OAuth1AccessTokenEndpoint(PistonRequestValidator()) + + def dispatch(self, request): + return oauthlib_response( + self.endpoint.create_access_token_response( + **oauthlib_request(request) + ) + ) + + class UserView(RetrieveAPIView): permission_classes = [IsAuthenticated] serializer_class = serializers.UserSerializer -- 2.20.1 From 746bfb66224e6a1065e5355ed63f9c4836b0a498 Mon Sep 17 00:00:00 2001 From: Radek Czajka Date: Fri, 8 Feb 2019 11:27:01 +0100 Subject: [PATCH 06/16] Summon the annoying banner. --- src/wolnelektury/static/css/annoy.css | 8 ++++---- src/wolnelektury/static/js/annoy.js | 6 +++--- src/wolnelektury/templates/annoy.html | 7 ++----- src/wolnelektury/templates/base/superbase.html | 1 + 4 files changed, 10 insertions(+), 12 deletions(-) diff --git a/src/wolnelektury/static/css/annoy.css b/src/wolnelektury/static/css/annoy.css index 5ee80da35..54dbfba57 100755 --- a/src/wolnelektury/static/css/annoy.css +++ b/src/wolnelektury/static/css/annoy.css @@ -6,8 +6,8 @@ z-index: 200; font-family: Arial, sans-serif; display: block; - padding: 2.6px 13px; - /*width: 1em;*/ + padding: .1em 1em; + width: 1em; text-align:center; border-radius: 0 0 0 1em; position: absolute; @@ -19,9 +19,9 @@ #annoy { font-size: 13px; line-height: 1.15em; - /*padding: 1em 5em 1em 0;*/ + padding: 1em 5em 1em 0; display: none; - /*background: orange;*/ + background: orange; font-family: Arial, sans-serif; position: relative; z-index: 200; diff --git a/src/wolnelektury/static/js/annoy.js b/src/wolnelektury/static/js/annoy.js index 3104097a9..d71f14676 100644 --- a/src/wolnelektury/static/js/annoy.js +++ b/src/wolnelektury/static/js/annoy.js @@ -6,18 +6,18 @@ $("#annoy-on").click(function(e) { e.preventDefault(); $("#annoy").slideDown('fast'); $(this).hide(); - if (Modernizr.localstorage) localStorage.removeItem("annoyed2013"); + if (Modernizr.localstorage) localStorage.removeItem("annoyed2019"); }); $("#annoy-off").click(function() { $("#annoy").slideUp('fast'); $("#annoy-on").show(); - if (Modernizr.localstorage) localStorage["annoyed2013"] = true; + if (Modernizr.localstorage) localStorage["annoyed2019"] = true; }); if (Modernizr.localstorage) { - if (!localStorage["annoyed2013"]) { + if (!localStorage["annoyed2019"]) { $("#annoy-on").hide(); $("#annoy").show(); } diff --git a/src/wolnelektury/templates/annoy.html b/src/wolnelektury/templates/annoy.html index ecf1a1f9d..1f0889908 100755 --- a/src/wolnelektury/templates/annoy.html +++ b/src/wolnelektury/templates/annoy.html @@ -1,8 +1,7 @@ {% load static %} -Misja Książka +1%
- {% comment %} - + Logo akcji 1%

Droga użytkowniczko, drogi użytkowniku!

@@ -16,8 +15,6 @@

Dowiedz się więcej

- {% endcomment %} x -
diff --git a/src/wolnelektury/templates/base/superbase.html b/src/wolnelektury/templates/base/superbase.html index 3ff768f33..f5feb4ed7 100644 --- a/src/wolnelektury/templates/base/superbase.html +++ b/src/wolnelektury/templates/base/superbase.html @@ -35,6 +35,7 @@ {% block bodycontent %} {% block annoy %} + {% include "annoy.html" %} {% if not funding_no_show_current %} {% cache 120 funding_top_bar LANGUAGE_CODE %} {% funding_top_bar %} -- 2.20.1 From 6ed1d78c6ca14fd002c7a00867d1964e0c4ea4a8 Mon Sep 17 00:00:00 2001 From: Radek Czajka Date: Fri, 8 Feb 2019 11:28:45 +0100 Subject: [PATCH 07/16] Add HotJar on few pages. --- src/catalogue/templates/catalogue/book_detail.html | 8 ++++++++ .../templates/catalogue/tagged_object_list.html | 9 +++++++++ src/wolnelektury/templates/hotjar.html | 11 +++++++++++ src/wolnelektury/templates/main_page.html | 5 +++++ 4 files changed, 33 insertions(+) create mode 100644 src/wolnelektury/templates/hotjar.html diff --git a/src/catalogue/templates/catalogue/book_detail.html b/src/catalogue/templates/catalogue/book_detail.html index 865aca2bb..ecae89e82 100644 --- a/src/catalogue/templates/catalogue/book_detail.html +++ b/src/catalogue/templates/catalogue/book_detail.html @@ -12,6 +12,14 @@ {% block bodyid %}book-detail{% endblock %} +{% block extrahead %} +{{ block.super }} +{% if request.path == '/katalog/lektura/pan-tadeusz/' %} +{% include "hotjar.html" %} +{% endif %} +{% endblock %} + + {% block body %} {% include 'catalogue/book_wide.html' %} diff --git a/src/catalogue/templates/catalogue/tagged_object_list.html b/src/catalogue/templates/catalogue/tagged_object_list.html index f4ba6991c..285af0ac3 100644 --- a/src/catalogue/templates/catalogue/tagged_object_list.html +++ b/src/catalogue/templates/catalogue/tagged_object_list.html @@ -5,6 +5,15 @@ {% block titleextra %}{% if tags %}{% title_from_tags tags %}{% elif list_type == 'gallery' %}{% trans "Gallery" %}{% elif list_type == 'audiobooks' %}{% trans "Audiobooks" %}{% else %}{% trans "Literature" %}{% endif %}{% endblock %} + +{% block extrahead %} +{{ block.super }} +{% if request.path == '/katalog/lektury/' or request.path == '/katalog/audiobooki/' %} +{% include "hotjar.html" %} +{% endif %} +{% endblock %} + + {% block bodyid %}tagged-object-list{% endblock %} {% block body %} diff --git a/src/wolnelektury/templates/hotjar.html b/src/wolnelektury/templates/hotjar.html new file mode 100644 index 000000000..03d8e37ab --- /dev/null +++ b/src/wolnelektury/templates/hotjar.html @@ -0,0 +1,11 @@ + + diff --git a/src/wolnelektury/templates/main_page.html b/src/wolnelektury/templates/main_page.html index 7912c2421..4a627e6c3 100755 --- a/src/wolnelektury/templates/main_page.html +++ b/src/wolnelektury/templates/main_page.html @@ -10,6 +10,11 @@ {% block title %}{% trans "Wolne Lektury internet library" %}{% endblock %} {% block ogtitle %}{% trans "Wolne Lektury internet library" %}{% endblock %} +{% block extrahead %} +{{ block.super }} +{% include "hotjar.html" %} +{% endblock %} + {% block body %} {% spaceless %} {% if cite %} -- 2.20.1 From 25cb0dced028c91d9a73d7faedae4d3e73162902 Mon Sep 17 00:00:00 2001 From: Radek Czajka Date: Fri, 8 Feb 2019 11:38:57 +0100 Subject: [PATCH 08/16] Deploy in Makefile. --- Makefile | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Makefile b/Makefile index 5ef3ba7e4..471127c17 100644 --- a/Makefile +++ b/Makefile @@ -1,3 +1,13 @@ +.PHONY: deploy test + + +deploy: src/wolnelektury/localsettings.py + git submodule update --init + pip install -r requirements/requirements.txt + src/manage.py migrate --noinput + src/manage.py collectstatic --noinput + + .ONESHELL: test: cd src -- 2.20.1 From 832bfbcc54cd38942d6df89e850d4268386d6241 Mon Sep 17 00:00:00 2001 From: Radek Czajka Date: Fri, 8 Feb 2019 12:46:25 +0100 Subject: [PATCH 09/16] Bring Piwik for API back. --- src/api/urls.py | 17 +++++++---- src/catalogue/api/urls.py | 61 +++++++++++++++++++++++++++------------ src/social/api/urls.py | 9 ++++-- src/stats/utils.py | 13 +++++++++ 4 files changed, 74 insertions(+), 26 deletions(-) diff --git a/src/api/urls.py b/src/api/urls.py index 8f9914113..16fdfb749 100644 --- a/src/api/urls.py +++ b/src/api/urls.py @@ -6,7 +6,7 @@ from django.conf.urls import url, include from django.views.decorators.csrf import csrf_exempt from django.views.generic import TemplateView import catalogue.views -from api import handlers +from stats.utils import piwik_track_view from api.piston_patch import oauth_user_auth from . import views @@ -23,11 +23,18 @@ urlpatterns = [ url(r'tag/(?P\d*?)/info\.html$', catalogue.views.tag_info), # reading data - url(r'^reading/(?P[a-z0-9-]+)/$', views.BookUserDataView.as_view(), name='api_reading'), - url(r'^reading/(?P[a-z0-9-]+)/(?P[a-z]+)/$', views.BookUserDataView.as_view(), name='api_reading'), - url(r'^username/$', views.UserView.as_view(), name='api_username'), + url(r'^reading/(?P[a-z0-9-]+)/$', + piwik_track_view(views.BookUserDataView.as_view()), + name='api_reading'), + url(r'^reading/(?P[a-z0-9-]+)/(?P[a-z]+)/$', + piwik_track_view(views.BookUserDataView.as_view()), + name='api_reading'), + url(r'^username/$', + piwik_track_view(views.UserView.as_view()), + name='api_username'), - url(r'^blog/$', views.BlogView.as_view()), + url(r'^blog/$', + piwik_track_view(views.BlogView.as_view())), url(r'^pictures/$', include('picture.api.urls')), url(r'^', include('social.api.urls')), diff --git a/src/catalogue/api/urls.py b/src/catalogue/api/urls.py index 07729aea8..cf73c2cff 100644 --- a/src/catalogue/api/urls.py +++ b/src/catalogue/api/urls.py @@ -3,6 +3,7 @@ # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information. # from django.conf.urls import include, url +from stats.utils import piwik_track_view from . import views @@ -12,45 +13,67 @@ paginate_re = r'(?:after/(?P[a-z0-9-]+)/)?(?:count/(?P[0-9]+)/)?$' urlpatterns = [ # books by collections - url(r'^collections/$', views.CollectionList.as_view(), name="api_collections"), + url(r'^collections/$', + piwik_track_view(views.CollectionList.as_view()), + name="api_collections"), url(r'^collections/(?P[^/]+)/$', - views.CollectionDetail.as_view(), name="collection-detail"), + piwik_track_view(views.CollectionDetail.as_view()), + name="collection-detail"), url(tags_re + r'books/' + paginate_re, - views.BookList.as_view(), name='catalogue_api_book_list'), + piwik_track_view(views.BookList.as_view()), + name='catalogue_api_book_list'), url(tags_re + r'parent_books/' + paginate_re, - views.BookList.as_view(), {"top_level": True}, name='catalogue_api_parent_book_list'), + piwik_track_view(views.BookList.as_view()), + {"top_level": True}, name='catalogue_api_parent_book_list'), url(tags_re + r'audiobooks/' + paginate_re, - views.BookList.as_view(), {"audiobooks": True}, name='catalogue_api_audiobook_list'), + piwik_track_view(views.BookList.as_view()), + {"audiobooks": True}, name='catalogue_api_audiobook_list'), url(tags_re + r'daisy/' + paginate_re, - views.BookList.as_view(), {"daisy": True}, name='catalogue_api_daisy_list'), + piwik_track_view(views.BookList.as_view()), + {"daisy": True}, name='catalogue_api_daisy_list'), url(r'^recommended/' + paginate_re, - views.BookList.as_view(), + piwik_track_view(views.BookList.as_view()), {"recommended": True}, name='catalogue_api_recommended_list'), url(r'^newest/$', - views.BookList.as_view(), - {"newest": True, "top_level": True, "count": 20}, name='catalogue_api_newest_list'), + piwik_track_view(views.BookList.as_view()), + {"newest": True, "top_level": True, "count": 20}, + name='catalogue_api_newest_list'), - url(r'^books/(?P[^/]+)/$', views.BookDetail.as_view(), name='catalogue_api_book'), + url(r'^books/(?P[^/]+)/$', + piwik_track_view(views.BookDetail.as_view()), + name='catalogue_api_book'), url(tags_re + r'ebooks/' + paginate_re, - views.EbookList.as_view(), + piwik_track_view(views.EbookList.as_view()), name='catalogue_api_ebook_list'), url(tags_re + r'parent_ebooks/' + paginate_re, - views.EbookList.as_view(), + piwik_track_view(views.EbookList.as_view()), {"top_level": True}, name='catalogue_api_parent_ebook_list'), - url(r'^filter-books/$', views.FilterBookList.as_view(), name='catalogue_api_filter_books'), + url(r'^filter-books/$', + piwik_track_view(views.FilterBookList.as_view()), + name='catalogue_api_filter_books'), - url(r'^epub/(?P[a-z0-9-]+)/$', views.EpubView.as_view(), name='catalogue_api_epub'), + url(r'^epub/(?P[a-z0-9-]+)/$', + piwik_track_view(views.EpubView.as_view()), + name='catalogue_api_epub'), - url(r'^preview/$', views.Preview.as_view(), name='catalogue_api_preview'), + url(r'^preview/$', + piwik_track_view(views.Preview.as_view()), + name='catalogue_api_preview'), - url(r'^(?P(?:(?:[a-z0-9-]+/){2}){1,6})fragments/$', views.FragmentList.as_view()), + url(r'^(?P(?:(?:[a-z0-9-]+/){2}){1,6})fragments/$', + piwik_track_view(views.FragmentList.as_view())), url(r'^books/(?P[a-z0-9-]+)/fragments/(?P[a-z0-9-]+)/$', - views.FragmentView.as_view(), name="catalogue_api_fragment"), + piwik_track_view(views.FragmentView.as_view()), + name="catalogue_api_fragment"), - url(r'^(?P[a-z]+)s/$', views.TagCategoryView.as_view(), name='catalogue_api_tag_list'), - url(r'^(?P[a-z]+)s/(?P[a-z0-9-]+)/$', views.TagView.as_view(), name="catalogue_api_tag"), + url(r'^(?P[a-z]+)s/$', + piwik_track_view(views.TagCategoryView.as_view()), + name='catalogue_api_tag_list'), + url(r'^(?P[a-z]+)s/(?P[a-z0-9-]+)/$', + piwik_track_view(views.TagView.as_view()), + name="catalogue_api_tag"), ] diff --git a/src/social/api/urls.py b/src/social/api/urls.py index 8ded909b4..5c633d805 100644 --- a/src/social/api/urls.py +++ b/src/social/api/urls.py @@ -3,10 +3,15 @@ # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information. # from django.conf.urls import include, url +from stats.utils import piwik_track_view from . import views urlpatterns = [ - url(r'^like/(?P[a-z0-9-]+)/$', views.LikeView.as_view(), name='social_api_like'), - url(r'^shelf/(?P[a-z]+)/$', views.ShelfView.as_view(), name='social_api_shelf'), + url(r'^like/(?P[a-z0-9-]+)/$', + piwik_track_view(views.LikeView.as_view()), + name='social_api_like'), + url(r'^shelf/(?P[a-z]+)/$', + piwik_track_view(views.ShelfView.as_view()), + name='social_api_shelf'), ] diff --git a/src/stats/utils.py b/src/stats/utils.py index fc473a6f0..68a01f568 100644 --- a/src/stats/utils.py +++ b/src/stats/utils.py @@ -62,3 +62,16 @@ def piwik_track(klass_or_method): return klass else: return wrap + + +def piwik_track_view(view): + if not getattr(settings, 'PIWIK_SITE_ID', 0): + return view + + def wrap(request, *args, **kwargs): + if getattr(request, 'piwik_track', True): + track_request.delay(piwik_url(request)) + return view(self, request, *args, **kwargs) + + update_wrapper(wrap, view) + return wrap -- 2.20.1 From 2633e1ad9f69bd056dd75e3af8ccf58104272585 Mon Sep 17 00:00:00 2001 From: Radek Czajka Date: Mon, 11 Feb 2019 11:51:34 +0100 Subject: [PATCH 10/16] fixup --- src/stats/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stats/utils.py b/src/stats/utils.py index 68a01f568..86ce54ccd 100644 --- a/src/stats/utils.py +++ b/src/stats/utils.py @@ -71,7 +71,7 @@ def piwik_track_view(view): def wrap(request, *args, **kwargs): if getattr(request, 'piwik_track', True): track_request.delay(piwik_url(request)) - return view(self, request, *args, **kwargs) + return view(request, *args, **kwargs) update_wrapper(wrap, view) return wrap -- 2.20.1 From 40377c9b3b045814857e1190047ef83334c1131f Mon Sep 17 00:00:00 2001 From: Radek Czajka Date: Tue, 12 Feb 2019 22:51:47 +0100 Subject: [PATCH 11/16] App compatibility fix + some error handling. --- src/api/piston_patch.py | 38 ++++++++++++++++++++++++++------------ src/api/tests/tests.py | 30 ++++++++++++++++++++++-------- 2 files changed, 48 insertions(+), 20 deletions(-) diff --git a/src/api/piston_patch.py b/src/api/piston_patch.py index 3c7e50f4f..6a80e15cd 100644 --- a/src/api/piston_patch.py +++ b/src/api/piston_patch.py @@ -2,10 +2,10 @@ # This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later. # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information. # -from oauthlib.oauth1 import AuthorizationEndpoint +from oauthlib.oauth1 import AuthorizationEndpoint, OAuth1Error from django.contrib.auth.decorators import login_required from django import forms -from django.http import HttpResponseRedirect +from django.http import HttpResponse, HttpResponseRedirect from django.shortcuts import render from .request_validator import PistonRequestValidator from .utils import oauthlib_request, oauthlib_response @@ -21,15 +21,26 @@ class OAuthAuthenticationForm(forms.Form): # removed authorize_access - redundant +class OAuth1AuthorizationEndpoint(AuthorizationEndpoint): + def create_verifier(self, request, credentials): + verifier = super(OAuth1AuthorizationEndpoint, self).create_verifier(request, credentials) + return { + 'oauth_token': verifier['oauth_token'], + } + + @login_required def oauth_user_auth(request): - endpoint = AuthorizationEndpoint(PistonRequestValidator()) + endpoint = OAuth1AuthorizationEndpoint(PistonRequestValidator()) if request.method == "GET": # Why not just get oauth_token here? # This is fairly straightforward, in't? - realms, credentials = endpoint.get_realms_and_credentials( - **oauthlib_request(request)) + try: + realms, credentials = endpoint.get_realms_and_credentials( + **oauthlib_request(request)) + except OAuth1Error as e: + return HttpResponse(e.message, status=400) callback = request.GET.get('oauth_callback') form = OAuthAuthenticationForm(initial={ @@ -40,11 +51,14 @@ def oauth_user_auth(request): return render(request, 'piston/authorize_token.html', {'form': form}) elif request.method == "POST": - response = oauthlib_response( - endpoint.create_authorization_response( - credentials={"user": request.user}, - **oauthlib_request(request) + try: + response = oauthlib_response( + endpoint.create_authorization_response( + credentials={"user": request.user}, + **oauthlib_request(request) + ) ) - ) - - return response + except OAuth1Error as e: + return HttpResponse(e.message, status=400) + else: + return response diff --git a/src/api/tests/tests.py b/src/api/tests/tests.py index c8e07d26c..298a79433 100644 --- a/src/api/tests/tests.py +++ b/src/api/tests/tests.py @@ -242,6 +242,8 @@ class OAuth1Tests(ApiTest): @classmethod def setUpClass(cls): cls.user = User.objects.create(username='test') + cls.user.set_password('test') + cls.user.save() cls.consumer_secret = 'len(quote(consumer secret))>=32' Consumer.objects.create( key='client', @@ -253,6 +255,7 @@ class OAuth1Tests(ApiTest): User.objects.all().delete() def test_create_token(self): + # Fetch request token. base_query = ("oauth_consumer_key=client&oauth_nonce=12345678&" "oauth_signature_method=HMAC-SHA1&oauth_timestamp={}&" "oauth_version=1.0".format(int(time()))) @@ -268,16 +271,26 @@ class OAuth1Tests(ApiTest): sign = quote(h) query = "{}&oauth_signature={}".format(base_query, sign) response = self.client.get('/api/oauth/request_token/?' + query) - request_token = parse_qs(response.content) + request_token_data = parse_qs(response.content) + request_token = request_token_data['oauth_token'][0] + request_token_secret = request_token_data['oauth_token_secret'][0] - Token.objects.filter( - key=request_token['oauth_token'][0], token_type=Token.REQUEST - ).update(user=self.user, is_approved=True) + # Request token authorization. + self.client.login(username='test', password='test') + response = self.client.get('/api/oauth/authorize/?oauth_token=%s&oauth_callback=test://oauth.callback/' % request_token) + post_data = response.context['form'].initial + response = self.client.post('/api/oauth/authorize/?' + urlencode(post_data)) + self.assertEqual( + response['Location'], + 'test://oauth.callback/?oauth_token=' + request_token + ) + + # Fetch access token. base_query = ("oauth_consumer_key=client&oauth_nonce=12345678&" "oauth_signature_method=HMAC-SHA1&oauth_timestamp={}&" "oauth_token={}&oauth_version=1.0".format( - int(time()), request_token['oauth_token'][0])) + int(time()), request_token)) raw = '&'.join([ 'GET', quote('http://testserver/api/oauth/access_token/', safe=''), @@ -285,7 +298,7 @@ class OAuth1Tests(ApiTest): ]) h = hmac.new( quote(self.consumer_secret) + '&' + - quote(request_token['oauth_token_secret'][0], safe=''), + quote(request_token_secret, safe=''), raw, hashlib.sha1 ).digest() @@ -293,11 +306,12 @@ class OAuth1Tests(ApiTest): sign = quote(h) query = u"{}&oauth_signature={}".format(base_query, sign) response = self.client.get(u'/api/oauth/access_token/?' + query) - access_token = parse_qs(response.content) + access_token_data = parse_qs(response.content) + access_token = access_token_data['oauth_token'][0] self.assertTrue( Token.objects.filter( - key=access_token['oauth_token'][0], + key=access_token, token_type=Token.ACCESS, user=self.user ).exists()) -- 2.20.1 From 32cf6526ef36a9aa0af0de8673f19a94cbf5409b Mon Sep 17 00:00:00 2001 From: Radek Czajka Date: Wed, 13 Feb 2019 00:41:37 +0100 Subject: [PATCH 12/16] API links fixes. --- src/api/templates/api/main.html | 10 +++++----- src/catalogue/api/urls.py | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/api/templates/api/main.html b/src/api/templates/api/main.html index b7e7b8f95..a2a67cd67 100755 --- a/src/api/templates/api/main.html +++ b/src/api/templates/api/main.html @@ -35,12 +35,12 @@

    -
  • +
  • {% url "catalogue_api_book_list" "" %} – {% trans "All books" %}
  • -
  • - {% url "api_audiobook_list" "" %} – {% trans "Audiobooks" %}
  • -
  • - {% url "api_daisy_list" "" %} – {% trans "DAISY" %}
  • +
  • + {% url "catalogue_api_audiobook_list" "" %} – {% trans "Audiobooks" %}
  • +
  • + {% url "catalogue_api_daisy_list" "" %} – {% trans "DAISY" %}
  • {% url "catalogue_api_tag_list" "author" %} – {% trans "List of all authors" %}
  • diff --git a/src/catalogue/api/urls.py b/src/catalogue/api/urls.py index cf73c2cff..d70338a53 100644 --- a/src/catalogue/api/urls.py +++ b/src/catalogue/api/urls.py @@ -15,7 +15,7 @@ urlpatterns = [ # books by collections url(r'^collections/$', piwik_track_view(views.CollectionList.as_view()), - name="api_collections"), + name="catalogue_api_collections"), url(r'^collections/(?P[^/]+)/$', piwik_track_view(views.CollectionDetail.as_view()), name="collection-detail"), -- 2.20.1 From e4ddfe1b3a3bfa982386284d9859c64862cb6c08 Mon Sep 17 00:00:00 2001 From: Radek Czajka Date: Thu, 14 Feb 2019 22:12:54 +0100 Subject: [PATCH 13/16] Remove piston, keep just the models. --- requirements/requirements-dev.txt | 2 - requirements/requirements.txt | 1 - .../migrations => api}/piston/__init__.py | 0 src/api/piston/admin.py | 7 +++ .../piston/migrations}/0001_initial.py | 0 src/api/piston/migrations/__init__.py | 0 src/api/piston/models.py | 56 +++++++++++++++++++ src/api/request_validator.py | 2 +- src/api/tests/tests.py | 2 +- src/api/views.py | 2 +- src/wolnelektury/settings/apps.py | 2 +- src/wolnelektury/settings/contrib.py | 1 - 12 files changed, 67 insertions(+), 8 deletions(-) rename src/{wolnelektury/migrations => api}/piston/__init__.py (100%) create mode 100644 src/api/piston/admin.py rename src/{wolnelektury/migrations/piston => api/piston/migrations}/0001_initial.py (100%) create mode 100644 src/api/piston/migrations/__init__.py create mode 100644 src/api/piston/models.py diff --git a/requirements/requirements-dev.txt b/requirements/requirements-dev.txt index 2be42f850..d68937188 100644 --- a/requirements/requirements-dev.txt +++ b/requirements/requirements-dev.txt @@ -2,7 +2,5 @@ django-debug-toolbar<1.10 django-debug-toolbar-template-timings -Fabric sphinx pyinotify -fnpdeploy>=0.2.3 diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 34f4d9a38..7122431cf 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -7,7 +7,6 @@ django-pipeline>=1.6,<1.7 jsmin fnp-django-pagination django-maintenancemode==0.10 -django-piston==0.2.2.1.2 jsonfield>=1.0.3,<1.1 django-picklefield>=1.0,<1.1 django-modeltranslation>=0.10,<0.11 diff --git a/src/wolnelektury/migrations/piston/__init__.py b/src/api/piston/__init__.py similarity index 100% rename from src/wolnelektury/migrations/piston/__init__.py rename to src/api/piston/__init__.py diff --git a/src/api/piston/admin.py b/src/api/piston/admin.py new file mode 100644 index 000000000..792f19629 --- /dev/null +++ b/src/api/piston/admin.py @@ -0,0 +1,7 @@ +from django.contrib import admin +from . import models + + +admin.site.register(models.Nonce) +admin.site.register(models.Consumer) +admin.site.register(models.Token) diff --git a/src/wolnelektury/migrations/piston/0001_initial.py b/src/api/piston/migrations/0001_initial.py similarity index 100% rename from src/wolnelektury/migrations/piston/0001_initial.py rename to src/api/piston/migrations/0001_initial.py diff --git a/src/api/piston/migrations/__init__.py b/src/api/piston/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/api/piston/models.py b/src/api/piston/models.py new file mode 100644 index 000000000..7bb106c81 --- /dev/null +++ b/src/api/piston/models.py @@ -0,0 +1,56 @@ +from django.conf import settings +from django.db import models + + +KEY_SIZE = 18 +SECRET_SIZE = 32 + +CONSUMER_STATES = ( + ('pending', 'Pending approval'), + ('accepted', 'Accepted'), + ('canceled', 'Canceled'), +) + + +class Nonce(models.Model): + token_key = models.CharField(max_length=KEY_SIZE) + consumer_key = models.CharField(max_length=KEY_SIZE) + key = models.CharField(max_length=255) + + def __unicode__(self): + return u"Nonce %s for %s" % (self.key, self.consumer_key) + + +class Resource(models.Model): + name = models.CharField(max_length=255) + url = models.TextField(max_length=2047) + is_readonly = models.BooleanField(default=True) + + +class Consumer(models.Model): + name = models.CharField(max_length=255) + description = models.TextField() + key = models.CharField(max_length=KEY_SIZE) + secret = models.CharField(max_length=SECRET_SIZE) + status = models.CharField(max_length=16, choices=CONSUMER_STATES, default='pending') + user = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, blank=True, related_name='consumers') + + def __unicode__(self): + return u"Consumer %s with key %s" % (self.name, self.key) + + +class Token(models.Model): + REQUEST = 1 + ACCESS = 2 + TOKEN_TYPES = ((REQUEST, u'Request'), (ACCESS, u'Access')) + + key = models.CharField(max_length=KEY_SIZE) + secret = models.CharField(max_length=SECRET_SIZE) + token_type = models.IntegerField(choices=TOKEN_TYPES) + timestamp = models.IntegerField() + is_approved = models.BooleanField(default=False) + user = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, blank=True, related_name='tokens') + consumer = models.ForeignKey(Consumer) + + def __unicode__(self): + return u"%s Token %s for %s" % (self.get_token_type_display(), self.key, self.consumer) diff --git a/src/api/request_validator.py b/src/api/request_validator.py index 916636b91..55bd6fbdb 100644 --- a/src/api/request_validator.py +++ b/src/api/request_validator.py @@ -4,7 +4,7 @@ # import time from oauthlib.oauth1 import RequestValidator -from piston.models import Consumer, Nonce, Token +from api.piston.models import Consumer, Nonce, Token class PistonRequestValidator(RequestValidator): diff --git a/src/api/tests/tests.py b/src/api/tests/tests.py index 298a79433..3b4d4c59e 100644 --- a/src/api/tests/tests.py +++ b/src/api/tests/tests.py @@ -17,7 +17,7 @@ from django.core.files.uploadedfile import SimpleUploadedFile from django.test import TestCase from django.test.utils import override_settings from mock import patch -from piston.models import Consumer, Token +from api.piston.models import Consumer, Token from catalogue.models import Book, Tag from picture.forms import PictureImportForm diff --git a/src/api/views.py b/src/api/views.py index 518445dd0..66c9258e8 100644 --- a/src/api/views.py +++ b/src/api/views.py @@ -6,7 +6,7 @@ from django.http import Http404 from django.views.generic.base import View from oauthlib.common import urlencode from oauthlib.oauth1 import RequestTokenEndpoint, AccessTokenEndpoint -from piston.models import KEY_SIZE, SECRET_SIZE +from api.piston.models import KEY_SIZE, SECRET_SIZE from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework.views import APIView diff --git a/src/wolnelektury/settings/apps.py b/src/wolnelektury/settings/apps.py index c3be29bbe..2c76f67ed 100644 --- a/src/wolnelektury/settings/apps.py +++ b/src/wolnelektury/settings/apps.py @@ -50,7 +50,7 @@ INSTALLED_APPS_CONTRIB = [ 'rest_framework', 'fnp_django_pagination', 'pipeline', - 'piston', + 'api.piston', 'piwik', 'sorl.thumbnail', 'kombu.transport.django', diff --git a/src/wolnelektury/settings/contrib.py b/src/wolnelektury/settings/contrib.py index a25ef72d9..f3e7b6ba6 100644 --- a/src/wolnelektury/settings/contrib.py +++ b/src/wolnelektury/settings/contrib.py @@ -15,7 +15,6 @@ MODELTRANSLATION_PREPOPULATE_LANGUAGE = 'pl' MIGRATION_MODULES = { 'getpaid': 'wolnelektury.migrations.getpaid', - 'piston': 'wolnelektury.migrations.piston', } GETPAID_ORDER_DESCRIPTION = "{% load funding_tags %}{{ order|sanitize_payment_title }}" -- 2.20.1 From b95b09260db87f6665bfcef3fb4f7c19f8acb666 Mon Sep 17 00:00:00 2001 From: Radek Czajka Date: Thu, 14 Feb 2019 22:21:19 +0100 Subject: [PATCH 14/16] Django 1.9. --- requirements/requirements.txt | 19 ++-- src/api/urls.py | 2 +- src/catalogue/models/book.py | 4 +- src/catalogue/models/collection.py | 1 - src/catalogue/urls.py | 103 ++++++++---------- src/catalogue/views.py | 1 - src/chunks/urls.py | 11 +- src/contact/admin.py | 8 +- src/contact/urls.py | 8 +- src/dictionary/urls.py | 7 +- src/funding/urls.py | 41 ++++--- src/funding/views.py | 4 +- src/infopages/urls.py | 10 +- src/lesmianator/urls.py | 19 ++-- src/libraries/urls.py | 14 +-- src/oai/handlers.py | 1 - src/oai/urls.py | 9 +- src/opds/urls.py | 7 +- src/paypal/tests.py | 16 ++- src/picture/models.py | 28 ----- src/picture/views.py | 21 ---- src/polls/urls.py | 10 +- src/push/urls.py | 8 +- src/reporting/urls.py | 14 +-- src/search/urls.py | 13 ++- src/social/urls.py | 31 ++---- src/sponsors/urls.py | 11 +- src/suggest/urls.py | 7 +- src/waiter/urls.py | 10 +- src/wolnelektury/settings/apps.py | 1 - src/wolnelektury/settings/basic.py | 8 +- src/wolnelektury/settings/static.py | 5 - .../templates/base/superbase.html | 2 +- src/wolnelektury/templates/openid/login.html | 18 +-- .../templates/socialaccount/connections.html | 12 +- 35 files changed, 204 insertions(+), 280 deletions(-) diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 7122431cf..fd98553b0 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -1,15 +1,15 @@ --i https://py.mdrn.pl:8443/simple/ +-i https://py.mdrn.pl/simple/ # django -Django>=1.8,<1.9 +Django>=1.9,<1.10 fnpdjango>=0.2.8,<0.3 django-pipeline>=1.6,<1.7 jsmin fnp-django-pagination django-maintenancemode==0.10 -jsonfield>=1.0.3,<1.1 +jsonfield==2.0.2 django-picklefield>=1.0,<1.1 -django-modeltranslation>=0.10,<0.11 +django-modeltranslation==0.11 django-allauth>=0.32,<0.33 django-extensions djangorestframework<3.7 @@ -20,12 +20,11 @@ oauthlib>=3.0.1,<3.1 pyyaml polib -django-babel +django-babel==0.5.0 pytz -django-honeypot -django-uni-form +django-honeypot==0.6.0 python-memcached django-piwik @@ -57,10 +56,12 @@ pyoai==2.4.4 ## egenix-mx-base # Doesn't play nice with mx in dist-packages. sunburnt -django-getpaid>=1.7,<1.8 +#django-getpaid==1.7.4 +-e git+https://github.com/django-getpaid/django-getpaid.git@v1.7.4#egg=django-getpaid + httplib2 Texml -django-ssify>=0.2.1,<0.3 +django-ssify>=0.2.6,<0.3 raven diff --git a/src/api/urls.py b/src/api/urls.py index 16fdfb749..9c8dab26f 100644 --- a/src/api/urls.py +++ b/src/api/urls.py @@ -36,7 +36,7 @@ urlpatterns = [ url(r'^blog/$', piwik_track_view(views.BlogView.as_view())), - url(r'^pictures/$', include('picture.api.urls')), + url(r'^pictures/', include('picture.api.urls')), url(r'^', include('social.api.urls')), url(r'^', include('catalogue.api.urls')), ] diff --git a/src/catalogue/models/book.py b/src/catalogue/models/book.py index b2289f667..068bae899 100644 --- a/src/catalogue/models/book.py +++ b/src/catalogue/models/book.py @@ -205,12 +205,12 @@ class Book(models.Model): @permalink def get_absolute_url(self): - return 'catalogue.views.book_detail', [self.slug] + return 'book_detail', [self.slug] @staticmethod @permalink def create_url(slug): - return 'catalogue.views.book_detail', [slug] + return 'book_detail', [slug] def gallery_path(self): return gallery_path(self.slug) diff --git a/src/catalogue/models/collection.py b/src/catalogue/models/collection.py index e95204341..b765abefe 100644 --- a/src/catalogue/models/collection.py +++ b/src/catalogue/models/collection.py @@ -40,7 +40,6 @@ class Collection(models.Model): def get_query(self): slugs = self.book_slugs.split() # allow URIs - # WTF slugs = [slug.rstrip('/').rsplit('/', 1)[-1] if '/' in slug else slug for slug in slugs] return models.Q(slug__in=slugs) diff --git a/src/catalogue/urls.py b/src/catalogue/urls.py index 50a4a4af9..8b37e0774 100644 --- a/src/catalogue/urls.py +++ b/src/catalogue/urls.py @@ -2,54 +2,47 @@ # This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later. # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information. # -from django.conf.urls import patterns, url +from django.conf.urls import url from django.db.models import Max from django.views.generic import ListView, RedirectView from catalogue.feeds import AudiobookFeed -from catalogue.views import CustomPDFFormView from catalogue.models import Book +from catalogue import views +import picture.views SLUG = r'[a-z0-9-]*' -urlpatterns = patterns( - 'picture.views', - - url(r'^obraz/strona/$', 'picture_page', name='picture_page'), +urlpatterns = [ + url(r'^obraz/strona/$', picture.views.picture_page, name='picture_page'), # pictures - currently pictures are coupled with catalogue, hence the url is here - url(r'^obraz/$', 'picture_list_thumb', name='picture_list_thumb'), - url(r'^obraz/(?P%s).html$' % SLUG, 'picture_viewer', name='picture_viewer'), - url(r'^obraz/(?P%s)/$' % SLUG, 'picture_detail'), + url(r'^obraz/$', picture.views.picture_list_thumb, name='picture_list_thumb'), + url(r'^obraz/(?P%s).html$' % SLUG, picture.views.picture_viewer, name='picture_viewer'), + url(r'^obraz/(?P%s)/$' % SLUG, picture.views.picture_detail), - url(r'^p/(?P\d+)/mini\.(?P.+)\.html', 'picture_mini', name='picture_mini'), - url(r'^p/(?P\d+)/short\.(?P.+)\.html', 'picture_short', name='picture_short'), - url(r'^pa/(?P\d+)/short\.(?P.+)\.html', 'picturearea_short', name='picture_area_short'), -) + url(r'^p/(?P\d+)/mini\.(?P.+)\.html', picture.views.picture_mini, name='picture_mini'), + url(r'^p/(?P\d+)/short\.(?P.+)\.html', picture.views.picture_short, name='picture_short'), + url(r'^pa/(?P\d+)/short\.(?P.+)\.html', picture.views.picturearea_short, name='picture_area_short'), -urlpatterns += patterns( - '', # old search page - redirected url(r'^szukaj/$', RedirectView.as_view( url='/szukaj/', query_string=True, permanent=True)), -) -urlpatterns += patterns( - 'catalogue.views', - url(r'^$', 'catalogue', name='catalogue'), + url(r'^$', views.catalogue, name='catalogue'), - url(r'^autor/$', 'tag_catalogue', {'category': 'author'}, name='author_catalogue'), - url(r'^epoka/$', 'tag_catalogue', {'category': 'epoch'}, name='epoch_catalogue'), - url(r'^gatunek/$', 'tag_catalogue', {'category': 'genre'}, name='genre_catalogue'), - url(r'^rodzaj/$', 'tag_catalogue', {'category': 'kind'}, name='kind_catalogue'), - url(r'^motyw/$', 'tag_catalogue', {'category': 'theme'}, name='theme_catalogue'), + url(r'^autor/$', views.tag_catalogue, {'category': 'author'}, name='author_catalogue'), + url(r'^epoka/$', views.tag_catalogue, {'category': 'epoch'}, name='epoch_catalogue'), + url(r'^gatunek/$', views.tag_catalogue, {'category': 'genre'}, name='genre_catalogue'), + url(r'^rodzaj/$', views.tag_catalogue, {'category': 'kind'}, name='kind_catalogue'), + url(r'^motyw/$', views.tag_catalogue, {'category': 'theme'}, name='theme_catalogue'), - url(r'^galeria/$', 'gallery', name='gallery'), - url(r'^kolekcje/$', 'collections', name='catalogue_collections'), + url(r'^galeria/$', views.gallery, name='gallery'), + url(r'^kolekcje/$', views.collections, name='catalogue_collections'), - url(r'^lektury/$', 'literature', name='book_list'), - url(r'^lektury/(?P[a-zA-Z0-9-]+)/$', 'collection', name='collection'), - url(r'^audiobooki/$', 'audiobooks', name='audiobook_list'), - url(r'^daisy/$', 'daisy_list', name='daisy_list'), + url(r'^lektury/$', views.literature, name='book_list'), + url(r'^lektury/(?P[a-zA-Z0-9-]+)/$', views.collection, name='collection'), + url(r'^audiobooki/$', views.audiobooks, name='audiobook_list'), + url(r'^daisy/$', views.daisy_list, name='daisy_list'), url(r'^nowe/$', ListView.as_view( queryset=Book.objects.filter(parent=None).order_by('-created_at'), template_name='catalogue/recent_list.html'), name='recent_list'), @@ -60,45 +53,45 @@ urlpatterns += patterns( queryset=Book.objects.filter(media__type='daisy').annotate(m=Max('media__uploaded_at')).order_by('-m'), template_name='catalogue/recent_daisy_list.html'), name='recent_daisy_list'), - url(r'^custompdf/(?P%s)/$' % SLUG, CustomPDFFormView(), name='custom_pdf_form'), + url(r'^custompdf/(?P%s)/$' % SLUG, views.CustomPDFFormView(), name='custom_pdf_form'), 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'), + url(r'^pobierz/(?P%s).(?P[a-z0-9]*)$' % SLUG, views.embargo_link, name='embargo_link'), # zip - url(r'^zip/pdf\.zip$', 'download_zip', {'format': 'pdf', 'slug': None}, 'download_zip_pdf'), - url(r'^zip/epub\.zip$', 'download_zip', {'format': 'epub', 'slug': None}, 'download_zip_epub'), - url(r'^zip/mobi\.zip$', 'download_zip', {'format': 'mobi', 'slug': None}, 'download_zip_mobi'), - url(r'^zip/mp3/(?P%s)\.zip' % SLUG, 'download_zip', {'format': 'mp3'}, 'download_zip_mp3'), - url(r'^zip/ogg/(?P%s)\.zip' % SLUG, 'download_zip', {'format': 'ogg'}, 'download_zip_ogg'), + url(r'^zip/pdf\.zip$', views.download_zip, {'format': 'pdf', 'slug': None}, 'download_zip_pdf'), + url(r'^zip/epub\.zip$', views.download_zip, {'format': 'epub', 'slug': None}, 'download_zip_epub'), + url(r'^zip/mobi\.zip$', views.download_zip, {'format': 'mobi', 'slug': None}, 'download_zip_mobi'), + url(r'^zip/mp3/(?P%s)\.zip' % SLUG, views.download_zip, {'format': 'mp3'}, 'download_zip_mp3'), + url(r'^zip/ogg/(?P%s)\.zip' % SLUG, views.download_zip, {'format': 'ogg'}, 'download_zip_ogg'), # Public interface. Do not change this URLs. - url(r'^lektura/(?P%s)\.html$' % SLUG, 'book_text', name='book_text'), - url(r'^lektura/(?P%s)/audiobook/$' % SLUG, 'player', name='book_player'), - url(r'^lektura/(?P%s)/$' % SLUG, 'book_detail', name='book_detail'), + url(r'^lektura/(?P%s)\.html$' % SLUG, views.book_text, name='book_text'), + url(r'^lektura/(?P%s)/audiobook/$' % SLUG, views.player, name='book_player'), + url(r'^lektura/(?P%s)/$' % SLUG, views.book_detail, name='book_detail'), url(r'^lektura/(?P%s)/motyw/(?P[a-zA-Z0-9-]+)/$' % SLUG, - 'book_fragments', name='book_fragments'), + views.book_fragments, name='book_fragments'), - url(r'^okladka-ridero/(?P%s).png$' % SLUG, 'ridero_cover'), - url(r'^isbn/(?P(pdf|epub|mobi|txt|html))/(?P%s)/' % SLUG, 'get_isbn'), + url(r'^okladka-ridero/(?P%s).png$' % SLUG, views.ridero_cover), + url(r'^isbn/(?P(pdf|epub|mobi|txt|html))/(?P%s)/' % SLUG, views.get_isbn), # Includes. - url(r'^b/(?P\d+)/mini\.(?P.+)\.html', 'book_mini', name='catalogue_book_mini'), - url(r'^b/(?P\d+)/mini_nolink\.(?P.+)\.html', 'book_mini', {'with_link': False}, + url(r'^b/(?P\d+)/mini\.(?P.+)\.html', views.book_mini, name='catalogue_book_mini'), + url(r'^b/(?P\d+)/mini_nolink\.(?P.+)\.html', views.book_mini, {'with_link': False}, name='catalogue_book_mini_nolink'), - url(r'^b/(?P\d+)/short\.(?P.+)\.html', 'book_short', name='catalogue_book_short'), - url(r'^b/(?P\d+)/wide\.(?P.+)\.html', 'book_wide', name='catalogue_book_wide'), - url(r'^f/(?P\d+)/promo\.(?P.+)\.html', 'fragment_promo', name='catalogue_fragment_promo'), - url(r'^f/(?P\d+)/short\.(?P.+)\.html', 'fragment_short', name='catalogue_fragment_short'), - url(r'^t/(?P\d+)/box\.(?P.+)\.html', 'tag_box', name='catalogue_tag_box'), - url(r'^c/(?P.+)/box\.(?P.+)\.html', 'collection_box', name='catalogue_collection_box'), + url(r'^b/(?P\d+)/short\.(?P.+)\.html', views.book_short, name='catalogue_book_short'), + url(r'^b/(?P\d+)/wide\.(?P.+)\.html', views.book_wide, name='catalogue_book_wide'), + url(r'^f/(?P\d+)/promo\.(?P.+)\.html', views.fragment_promo, name='catalogue_fragment_promo'), + url(r'^f/(?P\d+)/short\.(?P.+)\.html', views.fragment_short, name='catalogue_fragment_short'), + url(r'^t/(?P\d+)/box\.(?P.+)\.html', views.tag_box, name='catalogue_tag_box'), + url(r'^c/(?P.+)/box\.(?P.+)\.html', views.collection_box, name='catalogue_collection_box'), # This should be the last pattern. - url(r'^galeria/(?P[a-zA-Z0-9-/]*)/$', 'tagged_object_list', {'list_type': 'gallery'}, + url(r'^galeria/(?P[a-zA-Z0-9-/]*)/$', views.tagged_object_list, {'list_type': 'gallery'}, name='tagged_object_list_gallery'), - url(r'^audiobooki/(?P[a-zA-Z0-9-/]*)/$', 'tagged_object_list', {'list_type': 'audiobooks'}, + url(r'^audiobooki/(?P[a-zA-Z0-9-/]*)/$', views.tagged_object_list, {'list_type': 'audiobooks'}, name='tagged_object_list_audiobooks'), - url(r'^(?P[a-zA-Z0-9-/]*)/$', 'tagged_object_list', {'list_type': 'books'}, + url(r'^(?P[a-zA-Z0-9-/]*)/$', views.tagged_object_list, {'list_type': 'books'}, name='tagged_object_list'), -) +] diff --git a/src/catalogue/views.py b/src/catalogue/views.py index f6d6cd5eb..2e43bd081 100644 --- a/src/catalogue/views.py +++ b/src/catalogue/views.py @@ -52,7 +52,6 @@ def book_list(request, filters=None, template_name='catalogue/book_list.html', for tag in books_by_author: if books_by_author[tag]: books_nav.setdefault(tag.sort_key[0], []).append(tag) - # WTF: dlaczego nie include? return render_to_response(template_name, { 'rendered_nav': render_to_string(nav_template_name, {'books_nav': books_nav}), 'rendered_book_list': render_to_string(list_template_name, { diff --git a/src/chunks/urls.py b/src/chunks/urls.py index 89472f961..0536a18b9 100644 --- a/src/chunks/urls.py +++ b/src/chunks/urls.py @@ -2,9 +2,10 @@ # This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later. # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information. # -from django.conf.urls import patterns, url +from django.conf.urls import url +from . import views -urlpatterns = patterns( - 'chunks.views', - url(r'^chunk/(?P.+)\.(?P.+)\.html$', 'chunk', name='chunk'), -) + +urlpatterns = [ + url(r'^chunk/(?P.+)\.(?P.+)\.html$', views.chunk, name='chunk'), +] diff --git a/src/contact/admin.py b/src/contact/admin.py index af14c6b02..41095092e 100644 --- a/src/contact/admin.py +++ b/src/contact/admin.py @@ -5,7 +5,7 @@ import json from django.contrib import admin from django.utils.translation import ugettext as _ from django.utils.safestring import mark_safe -from django.conf.urls import patterns, url +from django.conf.urls import url from django.http import HttpResponse, Http404 from wolnelektury.utils import UnicodeCSVWriter @@ -104,12 +104,10 @@ class ContactAdmin(admin.ModelAdmin): return super(ContactAdmin, self).changelist_view(request, extra_context=context) def get_urls(self): - # urls = super(ContactAdmin, self).get_urls() - return patterns( - '', + return [ url(r'^extract/(?P[\w-]+)/(?P[\w-]+)/$', self.admin_site.admin_view(extract_view), name='contact_extract') - ) + super(ContactAdmin, self).get_urls() + ] + super(ContactAdmin, self).get_urls() def extract_view(request, form_tag, extract_type_slug): diff --git a/src/contact/urls.py b/src/contact/urls.py index f2ef9444e..c475cd429 100644 --- a/src/contact/urls.py +++ b/src/contact/urls.py @@ -1,11 +1,11 @@ # -*- coding: utf-8 -*- -from django.conf.urls import patterns, url +from django.conf.urls import url from . import views -urlpatterns = patterns( - 'contact.views', + +urlpatterns = [ url(r'^(?P[^/]+)/$', views.form, name='contact_form'), url(r'^(?P[^/]+)/thanks/$', views.thanks, name='contact_thanks'), url(r'^attachment/(?P\d+)/(?P[^/]+)/$', views.attachment, name='contact_attachment'), url(r'^results/(?P\d+)/(?P[0-9a-f]+)/', views.results, name='contact_results'), -) +] diff --git a/src/dictionary/urls.py b/src/dictionary/urls.py index 6df9a4785..a5582a17a 100755 --- a/src/dictionary/urls.py +++ b/src/dictionary/urls.py @@ -2,10 +2,9 @@ # This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later. # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information. # -from django.conf.urls import patterns, url +from django.conf.urls import url from dictionary.views import NotesView -urlpatterns = patterns( - 'dictionary.views', +urlpatterns = [ url(r'^$', NotesView.as_view(), name='dictionary_notes'), -) +] diff --git a/src/funding/urls.py b/src/funding/urls.py index 1f0d494eb..f8afc66bb 100644 --- a/src/funding/urls.py +++ b/src/funding/urls.py @@ -2,33 +2,30 @@ # This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later. # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information. # -from django.conf.urls import patterns, url, include +from django.conf.urls import url, include +from . import views -from .views import WLFundView, OfferDetailView, OfferListView, ThanksView, NoThanksView, CurrentView, \ - DisableNotifications +urlpatterns = [ + url(r'^$', views.CurrentView.as_view(), name='funding_current'), + url(r'^teraz/$', views.CurrentView.as_view()), + url(r'^teraz/(?P[^/]+)/$', views.CurrentView.as_view(), name='funding_current'), + url(r'^lektura/$', views.OfferListView.as_view(), name='funding'), + url(r'^lektura/(?P[^/]+)/$', views.OfferDetailView.as_view(), name='funding_offer'), + url(r'^pozostale/$', views.WLFundView.as_view(), name='funding_wlfund'), -urlpatterns = patterns( - 'funding.views', - url(r'^$', CurrentView.as_view(), name='funding_current'), - url(r'^teraz/$', CurrentView.as_view()), - url(r'^teraz/(?P[^/]+)/$', CurrentView.as_view(), name='funding_current'), - url(r'^lektura/$', OfferListView.as_view(), name='funding'), - url(r'^lektura/(?P[^/]+)/$', OfferDetailView.as_view(), name='funding_offer'), - url(r'^pozostale/$', WLFundView.as_view(), name='funding_wlfund'), + url(r'^dziekujemy/$', views.ThanksView.as_view(), name='funding_thanks'), + url(r'^niepowodzenie/$', views.NoThanksView.as_view(), name='funding_nothanks'), - url(r'^dziekujemy/$', ThanksView.as_view(), name='funding_thanks'), - url(r'^niepowodzenie/$', NoThanksView.as_view(), name='funding_nothanks'), - - url(r'^wylacz_email/$', DisableNotifications.as_view(), name='funding_disable_notifications'), + url(r'^wylacz_email/$', views.DisableNotifications.as_view(), name='funding_disable_notifications'), url(r'^getpaid/', include('getpaid.urls')), # Includes - url(r'^o/(?P\d+)/top-bar\.(?P.+)\.html$', 'top_bar', name='funding_top_bar'), - url(r'^o/(?P\d+)/detail-bar\.(?P.+)\.html$', 'detail_bar', name='funding_detail_bar'), - url(r'^o/(?P\d+)/list-bar\.(?P.+)\.html$', 'list_bar', name='funding_list_bar'), - url(r'^o/(?P\d+)/status\.(?P.+)\.html$', 'offer_status', name='funding_status'), - url(r'^o/(?P\d+)/status-more\.(?P.+)\.html$', 'offer_status_more', name='funding_status_more'), - url(r'^o/(?P\d+)/fundings/(?P\d+)\.(?P.+)\.html$', 'offer_fundings', name='funding_fundings'), -) + url(r'^o/(?P\d+)/top-bar\.(?P.+)\.html$', views.top_bar, name='funding_top_bar'), + url(r'^o/(?P\d+)/detail-bar\.(?P.+)\.html$', views.detail_bar, name='funding_detail_bar'), + url(r'^o/(?P\d+)/list-bar\.(?P.+)\.html$', views.list_bar, name='funding_list_bar'), + url(r'^o/(?P\d+)/status\.(?P.+)\.html$', views.offer_status, name='funding_status'), + url(r'^o/(?P\d+)/status-more\.(?P.+)\.html$', views.offer_status_more, name='funding_status_more'), + url(r'^o/(?P\d+)/fundings/(?P\d+)\.(?P.+)\.html$', views.offer_fundings, name='funding_fundings'), +] diff --git a/src/funding/views.py b/src/funding/views.py index 18000c05d..74530a4af 100644 --- a/src/funding/views.py +++ b/src/funding/views.py @@ -88,7 +88,9 @@ class OfferDetailView(FormView): raise Http404 return super(OfferDetailView, self).dispatch(request, slug) - def get_form(self, form_class): + def get_form(self, form_class=None): + if form_class is None: + form_class = self.get_form_class() if self.request.method == 'POST': return form_class(self.object, self.request.POST) else: diff --git a/src/infopages/urls.py b/src/infopages/urls.py index 37025a1e4..ffae43966 100755 --- a/src/infopages/urls.py +++ b/src/infopages/urls.py @@ -2,10 +2,10 @@ # This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later. # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information. # -from django.conf.urls import patterns, url +from django.conf.urls import url +from . import views -urlpatterns = patterns( - 'infopages.views', - url(r'^(?P[a-zA-Z0-9_-]+)/$', 'infopage', name='infopage'), -) +urlpatterns = [ + url(r'^(?P[a-zA-Z0-9_-]+)/$', views.infopage, name='infopage'), +] diff --git a/src/lesmianator/urls.py b/src/lesmianator/urls.py index 37e363f8c..2685e83e2 100644 --- a/src/lesmianator/urls.py +++ b/src/lesmianator/urls.py @@ -2,13 +2,14 @@ # This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later. # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information. # -from django.conf.urls import url, patterns +from django.conf.urls import url +from . import views -urlpatterns = patterns( - 'lesmianator.views', - url(r'^$', 'main_page', name='lesmianator'), - url(r'^wiersz/$', 'new_poem', name='new_poem'), - url(r'^lektura/(?P[a-z0-9-]+)/$', 'poem_from_book', name='poem_from_book'), - url(r'^polka/(?P[a-zA-Z0-9-]+)/$', 'poem_from_set', name='poem_from_set'), - url(r'^wiersz/(?P[a-zA-Z0-9-]+)/$', 'get_poem', name='get_poem'), -) + +urlpatterns = [ + url(r'^$', views.main_page, name='lesmianator'), + url(r'^wiersz/$', views.new_poem, name='new_poem'), + url(r'^lektura/(?P[a-z0-9-]+)/$', views.poem_from_book, name='poem_from_book'), + url(r'^polka/(?P[a-zA-Z0-9-]+)/$', views.poem_from_set, name='poem_from_set'), + url(r'^wiersz/(?P[a-zA-Z0-9-]+)/$', views.get_poem, name='get_poem'), +] diff --git a/src/libraries/urls.py b/src/libraries/urls.py index ae1635948..897da151f 100644 --- a/src/libraries/urls.py +++ b/src/libraries/urls.py @@ -2,12 +2,12 @@ # This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later. # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information. # -from django.conf.urls import patterns, url +from django.conf.urls import url +from . import views -urlpatterns = patterns( - 'libraries.views', - url(r'^$', 'main_view', name='libraries_main_view'), - url(r'^(?P[a-zA-Z0-9_-]+)$', 'catalog_view', name='libraries_catalog_view'), - url(r'^(?P[a-zA-Z0-9_-]+)/(?P[a-zA-Z0-9_-]+)$', 'library_view', name='libraries_library_view'), -) +urlpatterns = [ + url(r'^$', views.main_view, name='libraries_main_view'), + url(r'^(?P[a-zA-Z0-9_-]+)$', views.catalog_view, name='libraries_catalog_view'), + url(r'^(?P[a-zA-Z0-9_-]+)/(?P[a-zA-Z0-9_-]+)$', views.library_view, name='libraries_library_view'), +] diff --git a/src/oai/handlers.py b/src/oai/handlers.py index 6fbcd4271..0eeea8c0d 100644 --- a/src/oai/handlers.py +++ b/src/oai/handlers.py @@ -56,7 +56,6 @@ def nsdcterms(name): return '{%s}%s' % (NS_DCTERMS, name) -# WTF class Catalogue(common.ResumptionOAIPMH): TAG_CATEGORIES = ['author', 'epoch', 'kind', 'genre'] diff --git a/src/oai/urls.py b/src/oai/urls.py index 13ea9f12a..205d5e6c9 100644 --- a/src/oai/urls.py +++ b/src/oai/urls.py @@ -2,7 +2,10 @@ # This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later. # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information. # -from django.conf.urls import patterns, url +from django.conf.urls import url +from . import views -urlpatterns = patterns('oai.views', - url(r'^$', 'oaipmh', name='oaipmh')) + +urlpatterns = [ + url(r'^$', views.oaipmh, name='oaipmh') +] diff --git a/src/opds/urls.py b/src/opds/urls.py index edf928297..8f1059f1e 100644 --- a/src/opds/urls.py +++ b/src/opds/urls.py @@ -2,16 +2,15 @@ # This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later. # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information. # -from django.conf.urls import patterns, url +from django.conf.urls import url from opds.views import RootFeed, ByCategoryFeed, ByTagFeed, UserFeed, UserSetFeed, SearchFeed -urlpatterns = patterns( - 'opds.views', +urlpatterns = [ url(r'^$', RootFeed(), name="opds_authors"), url(r'^search/$', SearchFeed(), name="opds_search"), url(r'^user/$', UserFeed(), name="opds_user"), url(r'^set/(?P[a-zA-Z0-9-]+)/$', UserSetFeed(), name="opds_user_set"), url(r'^(?P[a-zA-Z0-9-]+)/$', ByCategoryFeed(), name="opds_by_category"), url(r'^(?P[a-zA-Z0-9-]+)/(?P[a-zA-Z0-9-]+)/$', ByTagFeed(), name="opds_by_tag"), -) +] diff --git a/src/paypal/tests.py b/src/paypal/tests.py index e38fd8476..a7badbe83 100644 --- a/src/paypal/tests.py +++ b/src/paypal/tests.py @@ -167,11 +167,15 @@ class PaypalTests(WLTestCase): self.client.login(username='test', password='test') BillingPlan.objects.create(amount=100) response = self.client.get('/paypal/app-return/?token=secret-token') - self.assertRedirects(response, 'wolnelekturyapp://paypal_return') + self.assertRedirects( + response, 'wolnelekturyapp://paypal_return', + fetch_redirect_response=False) # Repeated returns will not generate further agreements. response = self.client.get('/paypal/app-return/?token=secret-token') - self.assertRedirects(response, 'wolnelekturyapp://paypal_return') + self.assertRedirects( + response, 'wolnelekturyapp://paypal_return', + fetch_redirect_response=False) self.assertEqual(BillingAgreement.objects.all().count(), 1) self.assertTrue(user_is_subscribed(self.user)) @@ -185,7 +189,9 @@ class PaypalTests(WLTestCase): execute=Mock(return_value=Mock(id=None)))): self.client.get('/paypal/app-return/?token=secret-token') response = self.client.get('/paypal/app-return/?token=secret-token') - self.assertRedirects(response, 'wolnelekturyapp://paypal_error') + self.assertRedirects( + response, 'wolnelekturyapp://paypal_error', + fetch_redirect_response=False) # No agreement created in our DB if not executed successfully. self.assertEqual(BillingAgreement.objects.all().count(), 0) @@ -195,7 +201,9 @@ class PaypalTests(WLTestCase): execute=BillingAgreementMock.execute, find=Mock(side_effect=ResourceNotFound(None)))): response = self.client.get('/paypal/app-return/?token=secret-token') - self.assertRedirects(response, 'wolnelekturyapp://paypal_return') + self.assertRedirects( + response, 'wolnelekturyapp://paypal_return', + fetch_redirect_response=False) # Now the agreement exists in our DB, but is not active. self.assertEqual([b.active for b in BillingAgreement.objects.all()], [False]) diff --git a/src/picture/models.py b/src/picture/models.py index 643149a1a..e20325702 100644 --- a/src/picture/models.py +++ b/src/picture/models.py @@ -9,7 +9,6 @@ from sorl.thumbnail import ImageField from django.conf import settings from django.contrib.contenttypes.fields import GenericRelation from django.core.files.storage import FileSystemStorage -from django.utils.datastructures import SortedDict from slugify import slugify from ssify import flush_ssi_includes @@ -326,33 +325,6 @@ class Picture(models.Model): annotated.paste(annotation.resize((img.size[0], 40), Image.ANTIALIAS), (0, img.size[1])) return annotated - # WTF/unused - @classmethod - def picture_list(cls, filter=None): - """Generates a hierarchical listing of all pictures - Pictures are optionally filtered with a test function. - """ - - pics = cls.objects.all().order_by('sort_key').only('title', 'slug', 'image_file') - - if filter: - pics = pics.filter(filter).distinct() - - pics_by_author = SortedDict() - orphans = [] - for tag in catalogue.models.Tag.objects.filter(category='author'): - pics_by_author[tag] = [] - - for pic in pics.iterator(): - authors = list(pic.authors().only('pk')) - if authors: - for author in authors: - pics_by_author[author].append(pic) - else: - orphans.append(pic) - - return pics_by_author, orphans - @property def info(self): if not hasattr(self, '_info'): diff --git a/src/picture/views.py b/src/picture/views.py index 6ace13427..6f02007e0 100644 --- a/src/picture/views.py +++ b/src/picture/views.py @@ -10,23 +10,6 @@ from picture.models import Picture, PictureArea from catalogue.utils import split_tags from ssify import ssi_included from sponsors.models import Sponsor - - -# WTF/unused -# # was picture/picture_list.html list (without thumbs) -# def picture_list(request, filter=None, get_filter=None, template_name='catalogue/picture_list.html', -# cache_key=None, context=None): -# """ generates a listing of all books, optionally filtered with a test function """ -# -# if get_filter: -# filt = get_filter() -# pictures_by_author, orphans = Picture.picture_list(filt) -# books_nav = OrderedDict() -# for tag in pictures_by_author: -# if pictures_by_author[tag]: -# books_nav.setdefault(tag.sort_key[0], []).append(tag) -# -# return render_to_response(template_name, locals(), context_instance=RequestContext(request)) from wolnelektury.utils import ajax @@ -45,10 +28,6 @@ def picture_detail(request, slug): theme_things = split_tags(picture.related_themes()) - # categories = SortedDict() - # for tag in picture.tags.iterator(): - # categories.setdefault(tag.category, []).append(tag) - return render_to_response("picture/picture_detail.html", { 'picture': picture, 'themes': theme_things.get('theme', []), diff --git a/src/polls/urls.py b/src/polls/urls.py index 0bec684e5..8a5f62136 100644 --- a/src/polls/urls.py +++ b/src/polls/urls.py @@ -2,10 +2,10 @@ # This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later. # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information. # -from django.conf.urls import patterns, url +from django.conf.urls import url +from . import views -urlpatterns = patterns( - 'polls.views', - url(r'^(?P[^/]+)$', 'poll', name='poll'), -) +urlpatterns = [ + url(r'^(?P[^/]+)$', views.poll, name='poll'), +] diff --git a/src/push/urls.py b/src/push/urls.py index 70a8487be..173b2dd18 100644 --- a/src/push/urls.py +++ b/src/push/urls.py @@ -2,11 +2,11 @@ # This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later. # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information. # -from django.conf.urls import patterns, url - +from django.conf.urls import url from push import views -urlpatterns = ( + +urlpatterns = [ url(r'^wyslij/$', views.notification_form, name='notification_form'), url(r'^wyslane/$', views.notification_sent, name='notification_sent'), -) +] diff --git a/src/reporting/urls.py b/src/reporting/urls.py index 3b09f1152..f4a65f44b 100755 --- a/src/reporting/urls.py +++ b/src/reporting/urls.py @@ -2,12 +2,12 @@ # This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later. # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information. # -from django.conf.urls import patterns, url +from django.conf.urls import url +from . import views -urlpatterns = patterns( - 'reporting.views', - url(r'^$', 'stats_page', name='reporting_stats'), - url(r'^katalog.pdf$', 'catalogue_pdf', name='reporting_catalogue_pdf'), - url(r'^katalog.csv$', 'catalogue_csv', name='reporting_catalogue_csv'), -) +urlpatterns = [ + url(r'^$', views.stats_page, name='reporting_stats'), + url(r'^katalog.pdf$', views.catalogue_pdf, name='reporting_catalogue_pdf'), + url(r'^katalog.csv$', views.catalogue_csv, name='reporting_catalogue_csv'), +] diff --git a/src/search/urls.py b/src/search/urls.py index 5a59422b5..46914d6d2 100644 --- a/src/search/urls.py +++ b/src/search/urls.py @@ -2,10 +2,11 @@ # This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later. # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information. # -from django.conf.urls import patterns, url +from django.conf.urls import url +from . import views -urlpatterns = patterns( - 'search.views', - url(r'^$', 'main', name='wlsearch'), - url(r'^hint/$', 'hint', name='search_hint'), -) + +urlpatterns = [ + url(r'^$', views.main, name='wlsearch'), + url(r'^hint/$', views.hint, name='search_hint'), +] diff --git a/src/social/urls.py b/src/social/urls.py index f9222ce93..395e54ac1 100755 --- a/src/social/urls.py +++ b/src/social/urls.py @@ -2,26 +2,19 @@ # This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later. # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information. # -from django.conf.urls import patterns, url +from django.conf.urls import url from django.views.decorators.cache import never_cache -from social.views import ObjectSetsFormView +from . import views -urlpatterns = patterns( - 'social.views', - url(r'^lektura/(?P[a-z0-9-]+)/lubie/$', 'like_book', name='social_like_book'), - url(r'^lektura/(?P[a-z0-9-]+)/nie_lubie/$', 'unlike_book', name='social_unlike_book'), - url(r'^lektura/(?P[a-z0-9-]+)/polki/$', never_cache(ObjectSetsFormView()), name='social_book_sets'), - url(r'^polka/$', 'my_shelf', name='social_my_shelf'), - # Includes - url(r'^cite/(?P\d+)\.(?P.+)\.html$', 'cite', name='social_cite'), - url(r'^cite_main/(?P\d+)\.(?P.+)\.html$', 'cite', {'main': True}, name='social_cite_main'), - url(r'^cite_info/(?P\d+).html$', 'cite_info', name='social_cite_info'), +urlpatterns = [ + url(r'^lektura/(?P[a-z0-9-]+)/lubie/$', views.like_book, name='social_like_book'), + url(r'^lektura/(?P[a-z0-9-]+)/nie_lubie/$', views.unlike_book, name='social_unlike_book'), + url(r'^lektura/(?P[a-z0-9-]+)/polki/$', never_cache(views.ObjectSetsFormView()), name='social_book_sets'), + url(r'^polka/$', views.my_shelf, name='social_my_shelf'), - # url(r'^polki/(?P[a-zA-Z0-9-]+)/formaty/$', 'shelf_book_formats', name='shelf_book_formats'), - # url(r'^polki/(?P[a-zA-Z0-9-]+)/(?P%s)/usun$' % SLUG, 'remove_from_shelf', name='remove_from_shelf'), - # url(r'^polki/$', 'user_shelves', name='user_shelves'), - # url(r'^polki/(?P[a-zA-Z0-9-]+)/usun/$', 'delete_shelf', name='delete_shelf'), - # url(r'^polki/(?P[a-zA-Z0-9-]+)\.zip$', 'download_shelf', name='download_shelf'), - # url(r'^polki/nowa/$', 'new_set', name='new_set'), -) + # Includes + url(r'^cite/(?P\d+)\.(?P.+)\.html$', views.cite, name='social_cite'), + url(r'^cite_main/(?P\d+)\.(?P.+)\.html$', views.cite, {'main': True}, name='social_cite_main'), + url(r'^cite_info/(?P\d+).html$', views.cite_info, name='social_cite_info'), +] diff --git a/src/sponsors/urls.py b/src/sponsors/urls.py index 7c6bc2ce6..fe3898678 100644 --- a/src/sponsors/urls.py +++ b/src/sponsors/urls.py @@ -2,9 +2,10 @@ # This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later. # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information. # -from django.conf.urls import patterns, url +from django.conf.urls import url +from . import views -urlpatterns = patterns( - 'sponsors.views', - url(r'^page/(?P.+)\.html$', 'page', name='sponsor_page'), -) + +urlpatterns = [ + url(r'^page/(?P.+)\.html$', views.page, name='sponsor_page'), +] diff --git a/src/suggest/urls.py b/src/suggest/urls.py index 5e721d45e..167a1a82f 100644 --- a/src/suggest/urls.py +++ b/src/suggest/urls.py @@ -2,11 +2,10 @@ # This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later. # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information. # -from django.conf.urls import patterns, url +from django.conf.urls import url from suggest import views -urlpatterns = patterns( - '', +urlpatterns = [ url(r'^$', views.SuggestionFormView(), name='suggest'), url(r'^plan/$', views.PublishingSuggestionFormView(), name='suggest_publishing'), -) +] diff --git a/src/waiter/urls.py b/src/waiter/urls.py index 69ab03a85..51d5f6e95 100644 --- a/src/waiter/urls.py +++ b/src/waiter/urls.py @@ -2,9 +2,9 @@ # This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later. # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information. # -from django.conf.urls import patterns, url +from django.conf.urls import url +from waiter import views -urlpatterns = patterns( - 'waiter.views', - url(r'^(?P.*)$', 'wait', name='waiter'), -) +urlpatterns = [ + url(r'^(?P.*)$', views.wait, name='waiter'), +] diff --git a/src/wolnelektury/settings/apps.py b/src/wolnelektury/settings/apps.py index 2c76f67ed..c98944b39 100644 --- a/src/wolnelektury/settings/apps.py +++ b/src/wolnelektury/settings/apps.py @@ -68,7 +68,6 @@ INSTALLED_APPS_CONTRIB = [ 'django_gravatar', # allauth stuff - 'uni_form', 'allauth', 'allauth.account', 'allauth.socialaccount', diff --git a/src/wolnelektury/settings/basic.py b/src/wolnelektury/settings/basic.py index 7990f2535..ebadbb8e0 100644 --- a/src/wolnelektury/settings/basic.py +++ b/src/wolnelektury/settings/basic.py @@ -49,10 +49,10 @@ TEMPLATES = [{ ), 'context_processors': ( 'django.contrib.auth.context_processors.auth', - 'django.core.context_processors.debug', - 'django.core.context_processors.i18n', - 'django.core.context_processors.media', - 'django.core.context_processors.request', + 'django.template.context_processors.debug', + 'django.template.context_processors.i18n', + 'django.template.context_processors.media', + 'django.template.context_processors.request', 'wolnelektury.context_processors.extra_settings', 'search.context_processors.search_form', ), diff --git a/src/wolnelektury/settings/static.py b/src/wolnelektury/settings/static.py index 3c1997d59..146e35bae 100644 --- a/src/wolnelektury/settings/static.py +++ b/src/wolnelektury/settings/static.py @@ -33,9 +33,6 @@ PIPELINE = { 'css/annoy.css', - 'uni_form/uni-form.css', - 'uni_form/default.uni-form.css', - 'css/ui-lightness/jquery-ui-1.8.16.custom.css', 'css/tlite.css', @@ -112,8 +109,6 @@ PIPELINE = { 'js/search.js', 'funding/funding.js', - 'uni_form/uni-form.js', - 'js/annoy.js', ), 'output_filename': 'js/base.min.js', diff --git a/src/wolnelektury/templates/base/superbase.html b/src/wolnelektury/templates/base/superbase.html index f5feb4ed7..12a6565ea 100644 --- a/src/wolnelektury/templates/base/superbase.html +++ b/src/wolnelektury/templates/base/superbase.html @@ -68,7 +68,7 @@
    {% for lang in LANGUAGES %} -
    + {% ssi_csrf_token %} -
    - {% endblock %} diff --git a/src/wolnelektury/templates/socialaccount/connections.html b/src/wolnelektury/templates/socialaccount/connections.html index 0f107242a..681038601 100644 --- a/src/wolnelektury/templates/socialaccount/connections.html +++ b/src/wolnelektury/templates/socialaccount/connections.html @@ -13,31 +13,25 @@

    {% blocktrans %}You can sign in to your account using any of the following third party accounts:{% endblocktrans %}

    -
    + {% csrf_token %} -
    {% if form.non_field_errors %} -
    {{form.non_field_errors}}
    +
    {{form.non_field_errors}}
    {% endif %} {% for base_account in form.accounts %} {% with base_account.get_provider_account as account %} -
    -
    +

    {% endwith %} {% endfor %} -
    -
    -
    -- 2.20.1 From 72272dce664793c3f2910b99837fcd35efa8e815 Mon Sep 17 00:00:00 2001 From: Radek Czajka Date: Thu, 14 Feb 2019 22:48:39 +0100 Subject: [PATCH 15/16] Some forgotten Django 1.9 fixes. --- src/catalogue/templatetags/catalogue_tags.py | 17 +++++++++-------- src/chunks/templatetags/chunks.py | 3 ++- src/wolnelektury/urls.py | 15 ++++++++++----- 3 files changed, 21 insertions(+), 14 deletions(-) diff --git a/src/catalogue/templatetags/catalogue_tags.py b/src/catalogue/templatetags/catalogue_tags.py index 5efdb05e6..263f12cfa 100644 --- a/src/catalogue/templatetags/catalogue_tags.py +++ b/src/catalogue/templatetags/catalogue_tags.py @@ -11,6 +11,7 @@ from django import template from django.template import Node, Variable, Template, Context from django.core.urlresolvers import reverse from django.utils.cache import add_never_cache_headers +from django.utils.safestring import mark_safe from django.utils.translation import ugettext as _ from ssify import ssi_variable @@ -57,7 +58,7 @@ def simple_title(tags): @register.simple_tag def book_title(book, html_links=False): - return book.pretty_title(html_links) + return mark_safe(book.pretty_title(html_links)) @register.simple_tag @@ -136,7 +137,7 @@ def book_tree(book_list, books_by_parent): ) for book in book_list) if text: - return "
      %s
    " % text + return mark_safe("
      %s
    " % text) else: return '' @@ -149,14 +150,14 @@ def audiobook_tree(book_list, books_by_parent): ) for book in book_list) if text: - return "
      %s
    " % text + return mark_safe("
      %s
    " % text) else: return '' @register.simple_tag def book_tree_texml(book_list, books_by_parent, depth=1): - return "".join(""" + return mark_safe("".join(""" %(depth)dem%(title)s %(audiences)s %(audiobook)s @@ -168,7 +169,7 @@ def book_tree_texml(book_list, books_by_parent, depth=1): "audiences": ", ".join(book.audiences_pl()), "audiobook": "audiobook" if book.has_media('mp3') else "", "children": book_tree_texml(books_by_parent.get(book.id, ()), books_by_parent, depth + 1) - } for book in book_list) + } for book in book_list)) @register.simple_tag @@ -181,7 +182,7 @@ def book_tree_csv(author, book_list, books_by_parent, depth=1, max_depth=3, deli except ValueError: return s - return "".join("""%(author)s%(d)s%(preindent)s%(title)s%(d)s%(postindent)s%(audiences)s%(d)s%(audiobook)s + return mark_safe("".join("""%(author)s%(d)s%(preindent)s%(title)s%(d)s%(postindent)s%(audiences)s%(d)s%(audiobook)s %(children)s""" % { "d": delimeter, "preindent": delimeter * (depth - 1), @@ -192,7 +193,7 @@ def book_tree_csv(author, book_list, books_by_parent, depth=1, max_depth=3, deli "audiences": ", ".join(book.audiences_pl()), "audiobook": "audiobook" if book.has_media('mp3') else "", "children": book_tree_csv(author, books_by_parent.get(book.id, ()), books_by_parent, depth + 1) - } for book in book_list) + } for book in book_list)) @register.simple_tag @@ -415,7 +416,7 @@ def download_audio(book, daisy=True, mp3=True): if daisy and book.has_media('daisy'): for dsy in book.get_media('daisy'): links.append("%s" % (dsy.file.url, BookMedia.formats['daisy'].name)) - return "".join(links) + return mark_safe("".join(links)) @register.inclusion_tag("catalogue/snippets/custom_pdf_link_li.html") diff --git a/src/chunks/templatetags/chunks.py b/src/chunks/templatetags/chunks.py index 968d284fd..1d7362607 100644 --- a/src/chunks/templatetags/chunks.py +++ b/src/chunks/templatetags/chunks.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- from django import template from django.core.cache import cache +from django.utils.safestring import mark_safe from ..models import Chunk, Attachment @@ -20,7 +21,7 @@ def chunk(key, cache_time=0): n = Chunk(key=key) n.save() return '' - return content + return mark_safe(content) @register.simple_tag diff --git a/src/wolnelektury/urls.py b/src/wolnelektury/urls.py index 403cc1fc8..eb6a01785 100644 --- a/src/wolnelektury/urls.py +++ b/src/wolnelektury/urls.py @@ -67,11 +67,6 @@ urlpatterns += [ url(r'^szukaj/', include('search.urls')), - # Static files - url(r'^%s(?P.*)$' % settings.MEDIA_URL[1:], django.views.static.serve, - {'document_root': settings.MEDIA_ROOT, 'show_indexes': True}), - url(r'^%s(?P.*)$' % settings.STATIC_URL[1:], django.views.static.serve, - {'document_root': settings.STATIC_ROOT, 'show_indexes': True}), url(r'^i18n/', include('django.conf.urls.i18n')), ] @@ -97,3 +92,13 @@ urlpatterns += [ ] urlpatterns += migdal_urlpatterns + + +if settings.DEBUG: + urlpatterns += [ + # Static files + url(r'^%s(?P.*)$' % settings.MEDIA_URL[1:], django.views.static.serve, + {'document_root': settings.MEDIA_ROOT, 'show_indexes': True}), + url(r'^%s(?P.*)$' % settings.STATIC_URL[1:], django.views.static.serve, + {'document_root': settings.STATIC_ROOT, 'show_indexes': True}), + ] -- 2.20.1 From dd85755df4e3ef8489a6fb2f71a65abf84b06739 Mon Sep 17 00:00:00 2001 From: Radek Czajka Date: Thu, 14 Feb 2019 22:55:36 +0100 Subject: [PATCH 16/16] Admin shortcut bar fix. --- src/wolnelektury/templates/admin/base_site.html | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/wolnelektury/templates/admin/base_site.html b/src/wolnelektury/templates/admin/base_site.html index c4b16b482..9bd250c20 100644 --- a/src/wolnelektury/templates/admin/base_site.html +++ b/src/wolnelektury/templates/admin/base_site.html @@ -5,8 +5,7 @@ {% block branding %}

    {% trans "Site administration" %} - WolneLektury.pl

    -

    - +

    promobox | cytaty | info | @@ -19,4 +18,4 @@

    {% endblock %} -{% block nav-global %}{% endblock %} \ No newline at end of file +{% block nav-global %}{% endblock %} -- 2.20.1