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 %}
-
+
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 @@