Python 3
authorRadek Czajka <rczajka@rczajka.pl>
Tue, 12 Mar 2019 09:43:08 +0000 (10:43 +0100)
committerRadek Czajka <rczajka@rczajka.pl>
Tue, 12 Mar 2019 12:15:30 +0000 (13:15 +0100)
95 files changed:
Makefile
requirements/requirements-test.txt
requirements/requirements.txt
src/ajaxable/templatetags/ajaxable_tags.py
src/ajaxable/utils.py
src/api/handlers.py
src/api/models.py
src/api/tests/tests.py
src/api/utils.py
src/catalogue/api/views.py
src/catalogue/constants.py
src/catalogue/fields.py
src/catalogue/helpers.py
src/catalogue/management/commands/checkcovers.py
src/catalogue/management/commands/checkintegrity.py
src/catalogue/management/commands/gencover.py
src/catalogue/management/commands/importbooks.py
src/catalogue/management/commands/load_abstracts.py
src/catalogue/management/commands/pack.py
src/catalogue/management/commands/report_dead_links.py
src/catalogue/management/commands/savemedia.py [deleted file]
src/catalogue/management/commands/update_counters.py
src/catalogue/management/commands/update_tag_description.py
src/catalogue/models/book.py
src/catalogue/models/bookmedia.py
src/catalogue/models/collection.py
src/catalogue/models/source.py
src/catalogue/models/tag.py
src/catalogue/signals.py
src/catalogue/tasks.py
src/catalogue/templatetags/catalogue_tags.py
src/catalogue/test_utils.py
src/catalogue/tests/test_bookmedia.py
src/catalogue/tests/test_cover.py
src/catalogue/tests/test_tags.py
src/catalogue/translation.py
src/catalogue/utils.py
src/catalogue/views.py
src/chunks/models.py
src/club/admin.py
src/club/forms.py
src/club/models.py
src/contact/admin.py
src/contact/models.py
src/contact/views.py
src/dictionary/models.py
src/funding/management/commands/funding_notify.py
src/funding/models.py
src/funding/utils.py
src/infopages/models.py
src/isbn/forms.py
src/isbn/management/commands/export_onix.py
src/isbn/management/commands/import_onix.py
src/isbn/models.py
src/isbn/views.py
src/lesmianator/management/commands/lesmianator.py
src/lesmianator/models.py
src/libraries/models.py
src/newsletter/models.py
src/oai/handlers.py
src/opds/views.py
src/paypal/tests.py
src/paypal/views.py
src/pdcounter/models.py
src/picture/migrations/0005_auto_20141022_1001.py
src/picture/models.py
src/picture/tasks.py
src/picture/templatetags/picture_tags.py
src/picture/views.py
src/polls/models.py
src/push/models.py
src/reporting/utils.py
src/search/custom.py
src/search/fields.py
src/search/index.py
src/search/management/commands/reindex.py
src/search/management/commands/reindex_pictures.py
src/search/management/commands/snippets.py
src/search/mock_search.py
src/search/templatetags/search_tags.py
src/social/models.py
src/social/templatetags/social_tags.py
src/sortify.py
src/sponsors/models.py
src/sponsors/widgets.py
src/stats/tasks.py
src/stats/utils.py
src/suggest/models.py
src/wolnelektury/contact_forms.py
src/wolnelektury/management/commands/clean_social_accounts.py
src/wolnelektury/management/commands/localepack.py
src/wolnelektury/management/commands/translation2po.py
src/wolnelektury/middleware.py
src/wolnelektury/settings/apps.py
src/wolnelektury/utils.py

index 1a602ba..3956cf3 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -12,7 +12,6 @@ deploy: src/wolnelektury/localsettings.py
 test:
        cd src
        coverage run --branch --source='.' ./manage.py test; true
-       rm -rf ../htmlcov
        coverage html -d ../htmlcov.new
        rm -rf ../htmlcov
        mv ../htmlcov.new ../htmlcov
index 794632d..4ebc8ae 100644 (file)
@@ -1,4 +1 @@
--i https://py.mdrn.pl:8443/simple/
-
 coverage
-mock
index 267e56f..6ecaeb0 100644 (file)
@@ -38,7 +38,7 @@ mutagen>=1.31
 sorl-thumbnail>=12.3,<12.4
 
 # home-brewed & dependencies
-librarian==1.7.1
+librarian==1.7.2
 
 # celery tasks
 celery>=3.1.12,<3.2
@@ -48,10 +48,9 @@ kombu>=3.0.23,<3.1
 pyenchant
 
 # OAI-PMH
-pyoai==2.4.4
+pyoai==2.5.0
 
-## egenix-mx-base  # Doesn't play nice with mx in dist-packages.
-sunburnt
+scorched==0.12
 
 django-getpaid==1.8.0
 deprecated
index 31897d1..d327e2e 100644 (file)
@@ -3,7 +3,7 @@
 # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
 #
 from django import template
-from django.utils.encoding import force_unicode
+from django.utils.encoding import force_text
 from django.utils.safestring import mark_safe
 
 from ajaxable.utils import placeholdized
@@ -32,8 +32,8 @@ def pretty_field(field, template=None):
     return mark_safe(template % {
         'errors': field.errors,
         'input': field,
-        'label': ('*' if field.field.required else '') + force_unicode(field.label),
-        'helptext': force_unicode(field.help_text),
+        'label': ('*' if field.field.required else '') + force_text(field.label),
+        'helptext': force_text(field.help_text),
     })
 
 
index 89dd0e2..8e34a9a 100755 (executable)
@@ -6,7 +6,7 @@ from functools import wraps
 
 from django.http import HttpResponse, HttpResponseRedirect, HttpResponseForbidden
 from django.shortcuts import render
-from django.utils.encoding import force_unicode
+from django.utils.encoding import force_text
 from django.utils.functional import Promise
 from django.utils.http import urlquote_plus
 import json
@@ -18,7 +18,7 @@ from honeypot.decorators import verify_honeypot_value
 class LazyEncoder(json.JSONEncoder):
     def default(self, obj):
         if isinstance(obj, Promise):
-            return force_unicode(obj)
+            return force_text(obj)
         return obj
 
 
index d08812f..5af5cfb 100644 (file)
@@ -1,4 +1,3 @@
-# -*- 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.
 #
@@ -8,7 +7,7 @@ from catalogue.models import Book, Tag
 
 
 WL_BASE = lazy(
-    lambda: u'https://' + Site.objects.get_current().domain, unicode)()
+    lambda: 'https://' + Site.objects.get_current().domain, str)()
 
 category_singular = {
     'authors': 'author',
index 71dedb5..9a86c2c 100644 (file)
@@ -76,7 +76,7 @@ class Nonce(models.Model):
     consumer_key = models.CharField(max_length=KEY_SIZE)
     key = models.CharField(max_length=255)
 
-    def __unicode__(self):
+    def __str__(self):
         return u"Nonce %s for %s" % (self.key, self.consumer_key)
 
 
@@ -88,7 +88,7 @@ class Consumer(models.Model):
     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):
+    def __str__(self):
         return u"Consumer %s with key %s" % (self.name, self.key)
 
 
@@ -105,5 +105,5 @@ class Token(models.Model):
     user = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, blank=True, related_name='tokens')
     consumer = models.ForeignKey(Consumer)
 
-    def __unicode__(self):
+    def __str__(self):
         return u"%s Token %s for %s" % (self.get_token_type_display(), self.key, self.consumer)
index b3cc54f..eccdd04 100644 (file)
@@ -7,16 +7,15 @@ from os import path
 import hashlib
 import hmac
 import json
-from StringIO import StringIO
+from io import BytesIO
 from time import time
-from urllib import quote, urlencode
-from urlparse import parse_qs
+from urllib.parse import quote, urlencode, parse_qs
 
 from django.contrib.auth.models import User
 from django.core.files.uploadedfile import SimpleUploadedFile
 from django.test import TestCase
 from django.test.utils import override_settings
-from mock import patch
+from unittest.mock import patch
 from api.models import Consumer, Token
 
 from catalogue.models import Book, Tag
@@ -40,7 +39,7 @@ class ApiTest(TestCase):
         return data
 
     def assert_response(self, url, name):
-        content = self.client.get(url).content.rstrip()
+        content = self.client.get(url).content.decode('utf-8').rstrip()
         filename = path.join(path.dirname(__file__), 'res', 'responses', name)
         with open(filename) as f:
             good_content = f.read().rstrip()
@@ -112,12 +111,12 @@ class PictureTests(ApiTest):
             'composition8.xml',
             open(path.join(
                 picture.tests.__path__[0], "files", slug + ".xml"
-            )).read())
+            ), 'rb').read())
         img = SimpleUploadedFile(
             'kompozycja-8.png',
             open(path.join(
                 picture.tests.__path__[0], "files", slug + ".png"
-            )).read())
+            ), 'rb').read())
 
         import_form = PictureImportForm({}, {
             'picture_xml_file': xml,
@@ -265,13 +264,15 @@ class OAuth1Tests(ApiTest):
             quote(base_query, safe='')
         ])
         h = hmac.new(
-            quote(self.consumer_secret) + '&', raw, hashlib.sha1
+            (quote(self.consumer_secret) + '&').encode('latin1'),
+            raw.encode('latin1'),
+            hashlib.sha1
         ).digest()
-        h = b64encode(h).rstrip('\n')
+        h = b64encode(h).rstrip(b'\n')
         sign = quote(h)
         query = "{}&oauth_signature={}".format(base_query, sign)
         response = self.client.get('/api/oauth/request_token/?' + query)
-        request_token_data = parse_qs(response.content)
+        request_token_data = parse_qs(response.content.decode('latin1'))
         request_token = request_token_data['oauth_token'][0]
         request_token_secret = request_token_data['oauth_token_secret'][0]
 
@@ -297,16 +298,16 @@ class OAuth1Tests(ApiTest):
             quote(base_query, safe='')
         ])
         h = hmac.new(
-            quote(self.consumer_secret) + '&' +
-            quote(request_token_secret, safe=''),
-            raw,
+            (quote(self.consumer_secret) + '&' +
+             quote(request_token_secret, safe='')).encode('latin1'),
+            raw.encode('latin1'),
             hashlib.sha1
         ).digest()
-        h = b64encode(h).rstrip('\n')
+        h = b64encode(h).rstrip(b'\n')
         sign = quote(h)
         query = u"{}&oauth_signature={}".format(base_query, sign)
         response = self.client.get(u'/api/oauth/access_token/?' + query)
-        access_token_data = parse_qs(response.content)
+        access_token_data = parse_qs(response.content.decode('latin1'))
         access_token = access_token_data['oauth_token'][0]
 
         self.assertTrue(
@@ -333,7 +334,7 @@ class AuthorizedTests(ApiTest):
             consumer=cls.consumer,
             token_type=Token.ACCESS,
             timestamp=time())
-        cls.key = cls.consumer.secret + '&' + cls.token.secret
+        cls.key = (cls.consumer.secret + '&' + cls.token.secret).encode('latin1')
 
     @classmethod
     def tearDownClass(cls):
@@ -365,7 +366,10 @@ class AuthorizedTests(ApiTest):
                 for (k, v) in sorted(sign_params.items())))
         ])
         auth_params["oauth_signature"] = quote(b64encode(hmac.new(
-            self.key, raw, hashlib.sha1).digest()).rstrip('\n'))
+            self.key,
+            raw.encode('latin1'),
+            hashlib.sha1
+        ).digest()).rstrip(b'\n'))
         auth = 'OAuth realm="API", ' + ', '.join(
             '{}="{}"'.format(k, v) for (k, v) in auth_params.items())
 
@@ -447,10 +451,10 @@ class AuthorizedTests(ApiTest):
                 {"username": "test", "premium": True})
         with patch('paypal.permissions.user_is_subscribed', return_value=True):
             with patch('django.core.files.storage.Storage.open',
-                       return_value=StringIO("<epub>")):
+                       return_value=BytesIO(b"<epub>")):
                 self.assertEqual(
                     self.signed('/api/epub/grandchild/').content,
-                    "<epub>")
+                    b"<epub>")
 
     def test_publish(self):
         response = self.signed('/api/books/',
index c9c1f94..a3a1f89 100644 (file)
@@ -25,8 +25,9 @@ def oauthlib_request(request):
         "headers": headers,
     }
 
-def oauthlib_response((headers, body, status)):
+def oauthlib_response(response_tuple):
     """Creates a django.http.HttpResponse from (headers, body, status) tuple from OAuthlib."""
+    headers, body, status = response_tuple
     response = HttpResponse(body, status=status)
     for k, v in headers.items():
         if k == 'Location':
index 62876c0..7c4b888 100644 (file)
@@ -52,6 +52,11 @@ class BookList(ListAPIView):
         new_api = self.request.query_params.get('new_api')
         after = self.request.query_params.get('after', self.kwargs.get('after'))
         count = self.request.query_params.get('count', self.kwargs.get('count'))
+        if count:
+            try:
+                count = int(count)
+            except TypeError:
+                raise Http404  # Fixme
 
         if tags:
             if self.kwargs.get('top_level'):
index 60ceceb..1a655f4 100644 (file)
@@ -17,7 +17,8 @@ LICENSES = {
 LICENSES['http://creativecommons.org/licenses/by-sa/3.0/deed.pl'] = \
     LICENSES['http://creativecommons.org/licenses/by-sa/3.0/']
 
-for license, data in LICENSES.items():
+
+for license, data in list(LICENSES.items()):
     LICENSES[license.replace('http://', 'https://')] = data
 
 # Those will be generated only for books with own HTML.
index ebe4df1..2b2d6a6 100644 (file)
@@ -106,7 +106,7 @@ class BuildEbook(Task):
     def build(self, fieldfile):
         book = fieldfile.instance
         out = self.transform(book.wldocument(), fieldfile)
-        fieldfile.save(None, File(open(out.get_filename())), save=False)
+        fieldfile.save(None, File(open(out.get_filename(), 'rb')), save=False)
         self.set_file_permissions(fieldfile)
         if book.pk is not None:
             books = type(book).objects.filter(pk=book.pk)
index 4dd7bda..0330e69 100644 (file)
@@ -8,14 +8,14 @@ from django.core.cache import cache
 
 from .models import Tag, Book
 from os.path import getmtime
-import cPickle
+import pickle
 from collections import defaultdict
 
 
 BOOK_CATEGORIES = ('author', 'epoch', 'genre', 'kind')
 
 _COUNTERS = None
-_COUNTER_TIME = None
+_COUNTER_TIME = 0
 
 
 def get_top_level_related_tags(tags, categories=None):
@@ -28,10 +28,10 @@ def get_top_level_related_tags(tags, categories=None):
     global _COUNTERS, _COUNTER_TIME
     # First, check that we have a valid and recent version of the counters.
     if getmtime(settings.CATALOGUE_COUNTERS_FILE) > _COUNTER_TIME:
-        for i in xrange(10):
+        for i in range(10):
             try:
-                with open(settings.CATALOGUE_COUNTERS_FILE) as f:
-                    _COUNTERS = cPickle.load(f)
+                with open(settings.CATALOGUE_COUNTERS_FILE, 'rb') as f:
+                    _COUNTERS = pickle.load(f)
             except (EOFError, ValueError):
                 if i < 9:
                     continue
@@ -95,8 +95,8 @@ def update_counters():
         "next": dict(next_combinations),
     }
 
-    with open(settings.CATALOGUE_COUNTERS_FILE, 'w') as f:
-        cPickle.dump(counters, f)
+    with open(settings.CATALOGUE_COUNTERS_FILE, 'wb') as f:
+        pickle.dump(counters, f)
 
 
 def get_audiobook_tags():
index 2466728..66a69c7 100644 (file)
@@ -1,12 +1,10 @@
-# -*- 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 optparse import make_option
 from django.contrib.sites.models import Site
 from django.core.management.base import BaseCommand
-from catalogue import app_settings
 from django.utils.functional import lazy
+from catalogue import app_settings
 
 
 def ancestor_has_cover(book):
@@ -27,12 +25,13 @@ def full_url(obj):
 
 
 class Command(BaseCommand):
-    option_list = BaseCommand.option_list + (
-        make_option('-q', '--quiet', action='store_false', dest='verbose', default=True,
-                    help='Suppress output'),
-    )
     help = 'Checks cover sources and licenses.'
 
+    def add_arguments(self, parser):
+        parser.add_argument(
+                '-q', '--quiet', action='store_false', dest='verbose',
+                default=True, help='Suppress output')
+
     def handle(self, **options):
         from collections import defaultdict
         import re
@@ -47,7 +46,7 @@ class Command(BaseCommand):
         bad_license = defaultdict(list)
         no_license = []
 
-        re_license = re.compile(ur'.*,\s*(CC.*)')
+        re_license = re.compile(r'.*,\s*(CC.*)')
 
         redakcja_url = app_settings.REDAKCJA_URL
         good_license = re.compile("(%s)" % ")|(".join(
@@ -71,7 +70,7 @@ class Command(BaseCommand):
                     else:
                         no_license.append(book)
 
-        print """%d books with no covers, %d with inherited covers.
+        print("""%d books with no covers, %d with inherited covers.
 Bad licenses used: %s (%d covers without license).
 %d covers not from %s.
 """ % (
@@ -81,51 +80,51 @@ Bad licenses used: %s (%d covers without license).
             len(no_license),
             len(not_redakcja),
             redakcja_url,
-            )
+            ))
 
         if verbose:
             if bad_license:
-                print
-                print "Bad license:"
-                print "============"
+                print()
+                print("Bad license:")
+                print("============")
                 for lic, books in bad_license.items():
-                    print
-                    print lic
+                    print()
+                    print(lic)
                     for book in books:
-                        print full_url(book)
+                        print(full_url(book))
 
             if no_license:
-                print
-                print "No license:"
-                print "==========="
+                print()
+                print("No license:")
+                print("===========")
                 for book in no_license:
-                    print
-                    print full_url(book)
-                    print book.extra_info.get('cover_by')
-                    print book.extra_info.get('cover_source')
-                    print book.extra_info.get('cover_url')
+                    print()
+                    print(full_url(book))
+                    print(book.extra_info.get('cover_by'))
+                    print(book.extra_info.get('cover_source'))
+                    print(book.extra_info.get('cover_url'))
 
             if not_redakcja:
-                print
-                print "Not from Redakcja or source missing:"
-                print "===================================="
+                print()
+                print("Not from Redakcja or source missing:")
+                print("====================================")
                 for book in not_redakcja:
-                    print
-                    print full_url(book)
-                    print book.extra_info.get('cover_by')
-                    print book.extra_info.get('cover_source')
-                    print book.extra_info.get('cover_url')
+                    print()
+                    print(full_url(book))
+                    print(book.extra_info.get('cover_by'))
+                    print(book.extra_info.get('cover_source'))
+                    print(book.extra_info.get('cover_url'))
 
             if without_cover:
-                print
-                print "No cover:"
-                print "========="
+                print()
+                print("No cover:")
+                print("=========")
                 for book in without_cover:
-                    print full_url(book)
+                    print(full_url(book))
 
             if with_ancestral_cover:
-                print
-                print "With ancestral cover:"
-                print "====================="
+                print()
+                print("With ancestral cover:")
+                print("=====================")
                 for book in with_ancestral_cover:
-                    print full_url(book)
+                    print(full_url(book))
index 6f090bb..e7d5ffd 100644 (file)
@@ -1,23 +1,22 @@
-# -*- 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 optparse import make_option
 from django.core.management.base import BaseCommand
-
-from catalogue.models import Book
 from librarian import ParseError
+from catalogue.models import Book
 
 
 class Command(BaseCommand):
-    option_list = BaseCommand.option_list + (
-        make_option('-q', '--quiet', action='store_false', dest='verbose', default=True,
-                    help='Suppress output'),
-        make_option('-d', '--dry-run', action='store_true', dest='dry_run', default=False,
-                    help="Just check for problems, don't fix them"),
-    )
     help = 'Checks integrity of catalogue data.'
 
+    def add_arguments(self, parser):
+        parser.add_argument(
+                '-q', '--quiet', action='store_false', dest='verbose',
+                default=True, help='Suppress output')
+        parser.add_argument(
+                '-d', '--dry-run', action='store_true', dest='dry_run',
+                default=False, help="Just check for problems, don't fix them")
+
     def handle(self, **options):
         from django.db import transaction
 
@@ -29,21 +28,21 @@ class Command(BaseCommand):
                     info = book.wldocument().book_info
                 except ParseError:
                     if verbose:
-                        print "ERROR! Bad XML for book:", book.slug
-                        print "To resolve: republish."
-                        print
+                        print("ERROR! Bad XML for book:", book.slug)
+                        print("To resolve: republish.")
+                        print()
                 else:
                     should_be = [p.slug for p in info.parts]
                     is_now = [p.slug for p in book.children.all().order_by('parent_number')]
                     if should_be != is_now:
                         if verbose:
-                            print "ERROR! Wrong children for book:", book.slug
-                            # print "Is:       ", is_now
-                            # print "Should be:", should_be
+                            print("ERROR! Wrong children for book:", book.slug)
+                            # print("Is:       ", is_now)
+                            # print("Should be:", should_be)
                             from difflib import ndiff
-                            print '\n'.join(ndiff(is_now, should_be))
-                            print "To resolve: republish parent book."
-                            print
+                            print('\n'.join(ndiff(is_now, should_be)))
+                            print("To resolve: republish parent book.")
+                            print()
 
                 # Check for ancestry.
                 parents = []
@@ -54,14 +53,14 @@ class Command(BaseCommand):
                 ancestors = list(book.ancestor.all())
                 if set(ancestors) != set(parents):
                     if options['verbose']:
-                        print "Wrong ancestry for book:", book
-                        print "Is:       ", ", ".join(ancestors)
-                        print "Should be:", ", ".join(parents)
+                        print("Wrong ancestry for book:", book)
+                        print("Is:       ", ", ".join(ancestors))
+                        print("Should be:", ", ".join(parents))
                     if not options['dry_run']:
                         book.repopulate_ancestors()
                         if options['verbose']:
-                            print "Fixed."
+                            print("Fixed.")
                     if options['verbose']:
-                        print
+                        print()
 
                 # TODO: check metadata tags, reset counters
index ec010a5..c0d099b 100644 (file)
@@ -1,4 +1,3 @@
-# -*- 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.
 #
index 5280778..b8a9aa7 100644 (file)
@@ -1,40 +1,41 @@
-# -*- 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 os
 import sys
-from optparse import make_option
 from django.conf import settings
 from django.core.management.base import BaseCommand
 from django.core.management.color import color_style
 from django.core.files import File
 from django.db import transaction
 from librarian.picture import ImageStore
-# from wolnelektury.management.profile import profile
 
 from catalogue.models import Book
 from picture.models import Picture
-
 from search.index import Index
 
 
 class Command(BaseCommand):
-    option_list = BaseCommand.option_list + (
-        make_option('-q', '--quiet', action='store_false', dest='verbose', default=True,
-                    help='Verbosity level; 0=minimal output, 1=normal output, 2=all output'),
-        make_option('-f', '--force', action='store_true', dest='force', default=False,
-                    help='Overwrite works already in the catalogue'),
-        make_option('-D', '--dont-build', dest='dont_build',
-                    metavar="FORMAT,...",
-                    help="Skip building specified formats"),
-        make_option('-S', '--no-search-index', action='store_false', dest='search_index', default=True,
-                    help='Skip indexing imported works for search'),
-        make_option('-p', '--picture', action='store_true', dest='import_picture', default=False,
-                    help='Import pictures'),
-    )
     help = 'Imports books from the specified directories.'
-    args = 'directory [directory ...]'
+
+    def add_arguments(self, parser):
+        parser.add_argument(
+                '-q', '--quiet', action='store_false', dest='verbose', default=True,
+                help='Verbosity level; 0=minimal output, 1=normal output, 2=all output')
+        parser.add_argument(
+                '-f', '--force', action='store_true', dest='force',
+                default=False, help='Overwrite works already in the catalogue')
+        parser.add_argument(
+                '-D', '--dont-build', dest='dont_build', metavar="FORMAT,...",
+                help="Skip building specified formats")
+        parser.add_argument(
+                '-S', '--no-search-index', action='store_false',
+                dest='search_index', default=True,
+                help='Skip indexing imported works for search')
+        parser.add_argument(
+                '-p', '--picture', action='store_true', dest='import_picture',
+                default=False, help='Import pictures')
+        parser.add_argument('directory', nargs='+')
 
     def import_book(self, file_path, options):
         verbose = options.get('verbose')
@@ -54,24 +55,23 @@ class Command(BaseCommand):
                     save=False
                     )
                 if verbose:
-                    print "Importing %s.%s" % (file_base, ebook_format)
+                    print("Importing %s.%s" % (file_base, ebook_format))
         book.save()
 
     def import_picture(self, file_path, options, continue_on_error=True):
         try:
             image_store = ImageStore(os.path.dirname(file_path))
             picture = Picture.from_xml_file(file_path, image_store=image_store, overwrite=options.get('force'))
-        except Exception, ex:
+        except Exception as ex:
             if continue_on_error:
-                print "%s: %s" % (file_path, ex)
+                print("%s: %s" % (file_path, ex))
                 return
             else:
                 raise ex
         return picture
 
-    # @profile
     @transaction.atomic
-    def handle(self, *directories, **options):
+    def handle(self, **options):
         self.style = color_style()
 
         verbose = options.get('verbose')
@@ -82,16 +82,16 @@ class Command(BaseCommand):
             try:
                 index.index_tags()
                 index.index.commit()
-            except Exception, e:
+            except Exception as e:
                 index.index.rollback()
                 raise e
 
         files_imported = 0
         files_skipped = 0
 
-        for dir_name in directories:
+        for dir_name in options['directory']:
             if not os.path.isdir(dir_name):
-                print self.style.ERROR("%s: Not a directory. Skipping." % dir_name)
+                print(self.style.ERROR("%s: Not a directory. Skipping." % dir_name))
             else:
                 # files queue
                 files = sorted(os.listdir(dir_name))
@@ -106,7 +106,7 @@ class Command(BaseCommand):
                         continue
 
                     if verbose > 0:
-                        print "Parsing '%s'" % file_path
+                        print("Parsing '%s'" % file_path)
                     else:
                         sys.stdout.write('.')
                         sys.stdout.flush()
@@ -121,16 +121,16 @@ class Command(BaseCommand):
                         files_imported += 1
 
                     except (Book.AlreadyExists, Picture.AlreadyExists):
-                        print self.style.ERROR(
+                        print(self.style.ERROR(
                             '%s: Book or Picture already imported. Skipping. To overwrite use --force.' %
-                            file_path)
+                            file_path))
                         files_skipped += 1
 
-                    except Book.DoesNotExist, e:
+                    except Book.DoesNotExist as e:
                         if file_name not in postponed or postponed[file_name] < files_imported:
                             # push it back into the queue, maybe the missing child will show up
                             if verbose:
-                                print self.style.NOTICE('Waiting for missing children')
+                                print(self.style.NOTICE('Waiting for missing children'))
                             files.append(file_name)
                             postponed[file_name] = files_imported
                         else:
@@ -138,7 +138,7 @@ class Command(BaseCommand):
                             raise e
 
         # Print results
-        print
-        print "Results: %d files imported, %d skipped, %d total." % (
-            files_imported, files_skipped, files_imported + files_skipped)
-        print
+        print()
+        print("Results: %d files imported, %d skipped, %d total." % (
+            files_imported, files_skipped, files_imported + files_skipped))
+        print()
index b828974..19ff8c4 100644 (file)
@@ -10,6 +10,6 @@ from catalogue.models import Book
 class Command(BaseCommand):
     def handle(self, *args, **options):
         for b in Book.objects.order_by('slug'):
-            print b.slug
+            print(b.slug)
             b.load_abstract()
             b.save()
index 98ad7d8..181c2e6 100755 (executable)
@@ -1,30 +1,32 @@
-# -*- 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 optparse import make_option
-
+import zipfile
 from django.core.management.base import BaseCommand
 from django.core.management.color import color_style
-import zipfile
-
 from catalogue.models import Book, Tag
 
 
 class Command(BaseCommand):
-    option_list = BaseCommand.option_list + (
-        make_option('-t', '--tags', dest='tags', metavar='SLUG,...',
-                    help='Use only books tagged with this tags'),
-        make_option('-i', '--include', dest='include', metavar='SLUG,...',
-                    help='Include specific books by slug'),
-        make_option('-e', '--exclude', dest='exclude', metavar='SLUG,...',
-                    help='Exclude specific books by slug')
-    )
     help = 'Prepare ZIP package with files of given type.'
-    args = '[%s] output_path.zip' % '|'.join(Book.formats)
 
-    def handle(self, ftype, path, **options):
+    def add_arguments(self, parser):
+        parser.add_argument(
+                '-t', '--tags', dest='tags', metavar='SLUG,...',
+                help='Use only books tagged with this tags')
+        parser.add_argument(
+                '-i', '--include', dest='include', metavar='SLUG,...',
+                help='Include specific books by slug')
+        parser.add_argument(
+                '-e', '--exclude', dest='exclude', metavar='SLUG,...',
+                help='Exclude specific books by slug')
+        parser.add_argument('ftype', metavar='|'.join(Book.formats))
+        parser.add_argument('path', metavar='output_path.zip')
+
+    def handle(self, **options):
         self.style = color_style()
+        ftype = options['ftype']
+        path = options['path']
         verbose = int(options.get('verbosity'))
         tags = options.get('tags')
         include = options.get('include')
@@ -33,7 +35,7 @@ class Command(BaseCommand):
         if ftype in Book.formats:
             field = "%s_file" % ftype
         else:
-            print self.style.ERROR('Unknown file type.')
+            print(self.style.ERROR('Unknown file type.'))
             return
 
         books = []
@@ -54,11 +56,11 @@ class Command(BaseCommand):
         processed = skipped = 0
         for book in books:
             if verbose >= 2:
-                print 'Parsing', book.slug
+                print('Parsing', book.slug)
             content = getattr(book, field)
             if not content:
                 if verbose >= 1:
-                    print self.style.NOTICE('%s has no %s file' % (book.slug, ftype))
+                    print(self.style.NOTICE('%s has no %s file' % (book.slug, ftype)))
                 skipped += 1
                 continue
             archive.write(content.path, str('%s.%s' % (book.slug, ftype)))
@@ -67,11 +69,11 @@ class Command(BaseCommand):
 
         if not processed:
             if skipped:
-                print self.style.ERROR("No books with %s files found" % ftype)
+                print(self.style.ERROR("No books with %s files found" % ftype))
             else:
-                print self.style.ERROR("No books found")
+                print(self.style.ERROR("No books found"))
             return
 
         if verbose >= 1:
-            print "%d processed, %d skipped" % (processed, skipped)
-            print "Results written to %s" % path
+            print("%d processed, %d skipped" % (processed, skipped))
+            print("Results written to %s" % path)
index 1eb7312..a6ee2a9 100644 (file)
@@ -1,9 +1,6 @@
-# -*- 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 __future__ import print_function, unicode_literals
-
 from django.core.management.base import BaseCommand
 
 
@@ -13,7 +10,7 @@ class Command(BaseCommand):
     def handle(self, **options):
         from catalogue.models import Book
         from picture.models import Picture
-        from urllib2 import urlopen, HTTPError, URLError
+        from urllib.request import urlopen, HTTPError, URLError
         from django.core.urlresolvers import reverse
         from django.contrib.sites.models import Site
 
@@ -46,10 +43,10 @@ class Command(BaseCommand):
                     if url:
                         try:
                             urlopen(url)
-                        except (HTTPError, URLError, ValueError), e:
+                        except (HTTPError, URLError, ValueError) as e:
                             if clean:
                                 clean = False
-                                print(unicode(obj).encode('utf-8'))
+                                print(str(obj).encode('utf-8'))
                                 print(('Na stronie: https://%s%s' % (domain, obj.get_absolute_url())).encode('utf-8'))
                                 print(
                                     ('Administracja: https://%s%s' % (domain, reverse(admin_name, args=[obj.pk])))
diff --git a/src/catalogue/management/commands/savemedia.py b/src/catalogue/management/commands/savemedia.py
deleted file mode 100755 (executable)
index ab4da51..0000000
+++ /dev/null
@@ -1,47 +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.
-#
-import os.path
-
-from django.core.management.base import BaseCommand
-from django.db import transaction
-
-from catalogue.models import Book, BookMedia
-from catalogue.utils import ExistingFile
-
-
-class Command(BaseCommand):
-    help = "Saves uploaded media with a given book and a given name. " \
-           "If media has a source SHA1 info - matching media is replaced."
-    args = 'path slug name'
-
-    @transaction.atomic
-    def handle(self, *args, **options):
-        path, slug, name, part_name, index, parts_count = args
-        index = int(index)
-        parts_count = int(parts_count)
-
-        book = Book.objects.get(slug=slug)
-
-        root, ext = os.path.splitext(path)
-        ext = ext.lower()
-        if ext:
-            ext = ext[1:]
-            if ext == 'zip':
-                ext = 'daisy'
-
-        source_sha1 = BookMedia.read_source_sha1(path, ext)
-        print "Source file SHA1:", source_sha1
-        try:
-            assert source_sha1
-            bm = book.media.get(type=ext, source_sha1=source_sha1)
-            print "Replacing media: %s (%s)" % (bm.name.encode('utf-8'), ext)
-        except (AssertionError, BookMedia.DoesNotExist):
-            bm = BookMedia(book=book, type=ext)
-            print "Creating new media"
-        bm.name = name
-        bm.part_name = part_name
-        bm.index = index
-        bm.file.save(None, ExistingFile(path))
-        bm.save(parts_count=parts_count)
index 089dd0f..c79f614 100644 (file)
@@ -1,9 +1,6 @@
-# -*- 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 __future__ import print_function, unicode_literals
-
 from django.conf import settings
 from django.core.management.base import BaseCommand
 
index d13b120..bd00562 100644 (file)
@@ -1,15 +1,17 @@
-# -*- 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.core.management import BaseCommand
-
 from catalogue.models import Tag
 
 
 class Command(BaseCommand):
     help = "Update description for given tag."
-    args = 'category slug description_filename'
+
+    def add_arguments(self, parser):
+        parser.add_argument('category')
+        parser.add_argument('slug')
+        parser.add_argument('description_filename')
 
     def handle(self, category, slug, description_filename, **options):
         tag = Tag.objects.get(category=category, slug=slug)
index 923a604..206ab73 100644 (file)
@@ -127,7 +127,7 @@ class Book(models.Model):
         verbose_name_plural = _('books')
         app_label = 'catalogue'
 
-    def __unicode__(self):
+    def __str__(self):
         return self.title
 
     def get_initial(self):
@@ -188,7 +188,7 @@ class Book(models.Model):
         from sortify import sortify
 
         self.sort_key = sortify(self.title)[:120]
-        self.title = unicode(self.title)  # ???
+        self.title = str(self.title)  # ???
 
         try:
             author = self.authors().first().sort_key
@@ -401,7 +401,7 @@ class Book(models.Model):
                 index.index_tags()
             if commit:
                 index.index.commit()
-        except Exception, e:
+        except Exception as e:
             index.index.rollback()
             raise e
 
@@ -672,7 +672,7 @@ class Book(models.Model):
 
     def publisher(self):
         publisher = self.extra_info['publisher']
-        if isinstance(publisher, basestring):
+        if isinstance(publisher, str):
             return publisher
         elif isinstance(publisher, list):
             return ', '.join(publisher)
index 407c419..957e982 100644 (file)
@@ -38,7 +38,7 @@ class BookMedia(models.Model):
     book = models.ForeignKey('Book', related_name='media')
     source_sha1 = models.CharField(null=True, blank=True, max_length=40, editable=False)
 
-    def __unicode__(self):
+    def __str__(self):
         return "%s (%s)" % (self.name, self.file.name.split("/")[-1])
 
     class Meta:
@@ -77,7 +77,7 @@ class BookMedia(models.Model):
         remove_zip("%s_%s" % (self.book.slug, self.type))
 
         extra_info = self.extra_info
-        if isinstance(extra_info, basestring):
+        if isinstance(extra_info, str):
             # Walkaround for weird jsonfield 'no-decode' optimization.
             extra_info = json.loads(extra_info)
         extra_info.update(self.read_meta())
index b765abe..3c4b475 100644 (file)
@@ -24,7 +24,7 @@ class Collection(models.Model):
         verbose_name_plural = _('collections')
         app_label = 'catalogue'
 
-    def __unicode__(self):
+    def __str__(self):
         return self.title
 
     def get_initial(self):
index bcf5254..f36850d 100644 (file)
@@ -17,7 +17,7 @@ class Source(models.Model):
         verbose_name_plural = _('sources')
         app_label = 'catalogue'
 
-    def __unicode__(self):
+    def __str__(self):
         return self.netloc
 
     def save(self, *args, **kwargs):
index 31da256..1b41601 100644 (file)
@@ -43,7 +43,7 @@ class TagRelation(models.Model):
         db_table = 'catalogue_tag_relation'
         unique_together = (('tag', 'content_type', 'object_id'),)
 
-    def __unicode__(self):
+    def __str__(self):
         try:
             return u'%s [%s]' % (self.content_type.get_object_for_this_type(pk=self.object_id), self.tag)
         except ObjectDoesNotExist:
@@ -92,7 +92,7 @@ class Tag(models.Model):
         'polka': 'set',
         'obiekt': 'thing',
     }
-    categories_dict = dict((item[::-1] for item in categories_rev.iteritems()))
+    categories_dict = dict((item[::-1] for item in categories_rev.items()))
 
     class Meta:
         ordering = ('sort_key',)
@@ -155,7 +155,7 @@ class Tag(models.Model):
         flush_ssi_includes([
             '/katalog/%s.json' % lang for lang in languages])
 
-    def __unicode__(self):
+    def __str__(self):
         return self.name
 
     def __repr__(self):
@@ -234,7 +234,7 @@ class Tag(models.Model):
         for field_name, category in categories:
             try:
                 tag_names = getattr(info, field_name)
-            except KeyError:
+            except (AttributeError, KeyError):  # TODO: shouldn't be KeyError here at all.
                 try:
                     tag_names = [getattr(info, category)]
                 except KeyError:
@@ -250,7 +250,7 @@ class Tag(models.Model):
                     # Allow creating new tag, if it's in default language.
                     tag, created = Tag.objects.get_or_create(slug=slugify(tag_name), category=category)
                     if created:
-                        tag_name = unicode(tag_name)
+                        tag_name = str(tag_name)
                         tag.name = tag_name
                         setattr(tag, "name_%s" % lang, tag_name)
                         tag.sort_key = sortify(tag_sort_key.lower())
index 28d84be..d3d6ec3 100644 (file)
@@ -1,4 +1,3 @@
-# -*- 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.
 #
index 265897f..a7b67ae 100644 (file)
@@ -28,8 +28,8 @@ def index_book(book_id, book_info=None, **kwargs):
     from catalogue.models import Book
     try:
         return Book.objects.get(id=book_id).search_index(book_info, **kwargs)
-    except Exception, e:
-        print "Exception during index: %s" % e
+    except Exception as e:
+        print("Exception during index: %s" % e)
         print_exc()
         raise e
 
@@ -56,7 +56,7 @@ def build_custom_pdf(book_id, customizations, file_name, waiter_id=None):
                 morefloats=settings.LIBRARIAN_PDF_MOREFLOATS,
                 ilustr_path=gallery_path(wldoc.book_info.url.slug),
                 **kwargs)
-            DefaultStorage().save(file_name, File(open(pdf.get_filename())))
+            DefaultStorage().save(file_name, File(open(pdf.get_filename(), 'rb')))
     finally:
         if waiter_id is not None:
             WaitedFile.objects.filter(pk=waiter_id).delete()
index 263f12c..70676dd 100644 (file)
@@ -3,7 +3,7 @@
 # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
 #
 from random import randint, random
-from urlparse import urlparse
+from urllib.parse import urlparse
 from django.contrib.contenttypes.models import ContentType
 
 from django.conf import settings
@@ -93,39 +93,39 @@ def title_from_tags(tags):
     # Specjalny przypadek "Twórczość w pozytywizmie", wtedy gdy tylko epoka
     # jest wybrana przez użytkownika
     if 'epoch' in self and len(self) == 1:
-        text = u'Twórczość w %s' % flection.get_case(unicode(self['epoch']), u'miejscownik')
+        text = u'Twórczość w %s' % flection.get_case(str(self['epoch']), u'miejscownik')
         return capfirst(text)
 
     # Specjalny przypadek "Dramat w twórczości Sofoklesa", wtedy gdy podane
     # są tylko rodzaj literacki i autor
     if 'kind' in self and 'author' in self and len(self) == 2:
         text = u'%s w twórczości %s' % (
-            unicode(self['kind']), flection.get_case(unicode(self['author']), u'dopełniacz'))
+            str(self['kind']), flection.get_case(str(self['author']), u'dopełniacz'))
         return capfirst(text)
 
     # Przypadki ogólniejsze
     if 'theme' in self:
-        title += u'Motyw %s' % unicode(self['theme'])
+        title += u'Motyw %s' % str(self['theme'])
 
     if 'genre' in self:
         if 'theme' in self:
-            title += u' w %s' % flection.get_case(unicode(self['genre']), u'miejscownik')
+            title += u' w %s' % flection.get_case(str(self['genre']), u'miejscownik')
         else:
-            title += unicode(self['genre'])
+            title += str(self['genre'])
 
     if 'kind' in self or 'author' in self or 'epoch' in self:
         if 'genre' in self or 'theme' in self:
             if 'kind' in self:
-                title += u' w %s ' % flection.get_case(unicode(self['kind']), u'miejscownik')
+                title += u' w %s ' % flection.get_case(str(self['kind']), u'miejscownik')
             else:
                 title += u' w twórczości '
         else:
-            title += u'%s ' % unicode(self.get('kind', u'twórczość'))
+            title += u'%s ' % str(self.get('kind', u'twórczość'))
 
     if 'author' in self:
-        title += flection.get_case(unicode(self['author']), u'dopełniacz')
+        title += flection.get_case(str(self['author']), u'dopełniacz')
     elif 'epoch' in self:
-        title += flection.get_case(unicode(self['epoch']), u'dopełniacz')
+        title += flection.get_case(str(self['epoch']), u'dopełniacz')
 
     return capfirst(title)
 
index 8c76b89..7ed3536 100644 (file)
@@ -58,25 +58,25 @@ class BookInfoStub(object):
     def __getattr__(self, key):
         try:
             return self.__dict[key]
-        except KeyError:
+        except KeyError as e:
             if key in self._empty_fields:
                 return None
             elif key in self._salias:
                 return [getattr(self, self._salias[key])]
             else:
-                raise
+                raise AttributeError(e)
 
     def to_dict(self):
-        return dict((key, unicode(value)) for key, value in self.__dict.items())
+        return dict((key, str(value)) for key, value in self.__dict.items())
 
 
 def info_args(title, language=None):
     """ generate some keywords for comfortable BookInfoCreation  """
-    slug = unicode(slugify(title))
+    slug = str(slugify(title))
     if language is None:
         language = u'pol'
     return {
-        'title': unicode(title),
+        'title': str(title),
         'url': WLURI.from_slug(slug),
         'about': u"http://wolnelektury.pl/example/URI/%s" % slug,
         'language': language,
index 263e48d..b744f96 100644 (file)
@@ -15,8 +15,8 @@ class BookMediaTests(WLTestCase):
 
     def setUp(self):
         WLTestCase.setUp(self)
-        self.file = ContentFile('X')
-        self.file2 = ContentFile('Y')
+        self.file = ContentFile(b'X')
+        self.file2 = ContentFile(b'Y')
         self.book = models.Book.objects.create(slug='test-book', title='Test')
 
     def set_title(self, title):
@@ -50,7 +50,7 @@ class BookMediaTests(WLTestCase):
         bm.file.save(None, self.file)
         bm.file.save(None, self.file2)
 
-        self.assertEqual(bm.file.read(), 'Y')
+        self.assertEqual(bm.file.read(), b'Y')
         self.assertEqual(basename(bm.file.name), 'some-media.ogg')
 
     @skip('broken, but is it needed?')
@@ -67,8 +67,8 @@ class BookMediaTests(WLTestCase):
         bm2.file.save(None, self.file2)
         self.assertEqual(basename(bm.file.name), 'tytul.ogg')
         self.assertNotEqual(basename(bm2.file.name), 'tytul.ogg')
-        self.assertEqual(bm.file.read(), 'X')
-        self.assertEqual(bm2.file.read(), 'Y')
+        self.assertEqual(bm.file.read(), b'X')
+        self.assertEqual(bm2.file.read(), b'Y')
 
     def test_change_name(self):
         """
@@ -81,7 +81,7 @@ class BookMediaTests(WLTestCase):
         self.set_title("Other Title")
         bm.save()
         self.assertEqual(basename(bm.file.name), 'other-title.ogg')
-        self.assertEqual(bm.file.read(), 'X')
+        self.assertEqual(bm.file.read(), b'X')
 
     @skip('broken, but is it needed?')
     def test_change_name_no_clobber(self):
@@ -99,8 +99,8 @@ class BookMediaTests(WLTestCase):
         self.set_title("Title")
         bm2.save()
         self.assertNotEqual(basename(bm2.file.name), 'title.ogg')
-        self.assertEqual(bm.file.read(), 'X')
-        self.assertEqual(bm2.file.read(), 'Y')
+        self.assertEqual(bm.file.read(), b'X')
+        self.assertEqual(bm2.file.read(), b'Y')
 
     def test_zip_audiobooks(self):
         paths = [
index 8c5d047..59a05ed 100644 (file)
@@ -5,7 +5,7 @@
 from django.core.files.base import ContentFile
 from catalogue.test_utils import BookInfoStub, PersonStub, info_args, WLTestCase
 from catalogue.models import Book
-from mock import patch
+from unittest.mock import patch
 
 
 class CoverTests(WLTestCase):
index d5aa72c..7717782 100644 (file)
@@ -29,7 +29,7 @@ class BooksByTagTests(WLTestCase):
                                         parts=[self.child_info.url],
                                         **info_args("Parent"))
 
-        self.book_file = ContentFile('<utwor />')
+        self.book_file = ContentFile(b'<utwor />')
 
     def test_nonexistent_tag(self):
         """ Looking for a non-existent tag should yield 404 """
@@ -95,8 +95,10 @@ class TagRelatedTagsTests(WLTestCase):
                     Ala ma kota
                 <end id="m01" />
                 </akap></opowiadanie></utwor>
-                """ % info.title.encode('utf-8')
-            book = models.Book.from_text_and_meta(ContentFile(book_text), info)
+                """ % info.title
+            book = models.Book.from_text_and_meta(
+                    ContentFile(book_text.encode('utf-8')),
+                    info)
             book.save()
 
         tag_empty = models.Tag(name='Empty tag', slug='empty', category='author')
@@ -157,7 +159,7 @@ class TagRelatedTagsTests(WLTestCase):
         self.assertTrue(
             ('ChildKind', 2) in [(tag.name, tag.count) for tag in cats['kind']],
             'wrong related kind tags on tag page, got: ' +
-            unicode([(tag.name, tag.count) for tag in cats['kind']]))
+            str([(tag.name, tag.count) for tag in cats['kind']]))
 
         # all occurencies of theme should be counted
         self.assertTrue(('Theme', 4) in [(tag.name, tag.count) for tag in cats['theme']],
@@ -171,7 +173,7 @@ class TagRelatedTagsTests(WLTestCase):
         cats = self.client.get('/katalog/gatunek/childgenre/').context['categories']
         self.assertTrue(('Epoch', 2) in [(tag.name, tag.count) for tag in cats['epoch']],
                         'wrong related kind tags on tag page, got: ' +
-                        unicode([(tag.name, tag.count) for tag in cats['epoch']]))
+                        str([(tag.name, tag.count) for tag in cats['epoch']]))
 
 
 class CleanTagRelationTests(WLTestCase):
@@ -187,7 +189,9 @@ class CleanTagRelationTests(WLTestCase):
             <end id="m01" />
             </akap></opowiadanie></utwor>
             """
-        self.book = models.Book.from_text_and_meta(ContentFile(book_text), book_info)
+        self.book = models.Book.from_text_and_meta(
+                ContentFile(book_text.encode('utf-8')),
+                book_info)
 
     @skip('Not implemented and not priority')
     def test_delete_objects(self):
@@ -228,7 +232,9 @@ class TestIdenticalTag(WLTestCase):
 
     def test_book_tags(self):
         """ there should be all related tags in relevant categories """
-        book = models.Book.from_text_and_meta(ContentFile(self.book_text), self.book_info)
+        book = models.Book.from_text_and_meta(
+                ContentFile(self.book_text.encode('utf-8')),
+                self.book_info)
 
         related_themes = book.related_themes()
         for category in 'author', 'kind', 'genre', 'epoch':
@@ -237,9 +243,11 @@ class TestIdenticalTag(WLTestCase):
         self.assertTrue('tag' in [tag.slug for tag in related_themes])
 
     def test_qualified_url(self):
-        models.Book.from_text_and_meta(ContentFile(self.book_text), self.book_info)
+        models.Book.from_text_and_meta(
+                ContentFile(self.book_text.encode('utf-8')),
+                self.book_info)
         categories = {'author': 'autor', 'theme': 'motyw', 'epoch': 'epoka', 'kind': 'rodzaj', 'genre': 'gatunek'}
-        for cat, localcat in categories.iteritems():
+        for cat, localcat in categories.items():
             context = self.client.get('/katalog/%s/tag/' % localcat).context
             self.assertEqual(1, len(context['object_list']))
             self.assertNotEqual({}, context['categories'])
@@ -267,8 +275,10 @@ class BookTagsTests(WLTestCase):
                     Ala ma kota
                 <end id="m01" />
                 </akap></opowiadanie></utwor>
-                """ % info.title.encode('utf-8')
-            models.Book.from_text_and_meta(ContentFile(book_text), info)
+                """ % info.title
+            models.Book.from_text_and_meta(
+                    ContentFile(book_text.encode('utf-8')),
+                    info)
 
     def test_book_tags(self):
         """ book should have own tags and whole tree's themes """
index 4946143..aec44bc 100644 (file)
@@ -1,4 +1,3 @@
-# -*- 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.
 #
index 380ec8e..ec938ea 100644 (file)
@@ -18,7 +18,7 @@ from django.conf import settings
 from django.core.files.storage import DefaultStorage
 from django.core.files.uploadedfile import UploadedFile
 from django.http import HttpResponse
-from django.utils.encoding import force_unicode
+from django.utils.encoding import force_text
 
 from reporting.utils import read_chunks
 
@@ -27,14 +27,19 @@ if hasattr(random, 'SystemRandom'):
     randrange = random.SystemRandom().randrange
 else:
     randrange = random.randrange
-MAX_SESSION_KEY = 18446744073709551616L     # 2 << 63
+MAX_SESSION_KEY = 18446744073709551616     # 2 << 63
 
 
 def get_random_hash(seed):
-    sha_digest = hashlib.sha1('%s%s%s%s' % (
-        randrange(0, MAX_SESSION_KEY), time.time(), unicode(seed).encode('utf-8', 'replace'), settings.SECRET_KEY)
-    ).digest()
-    return urlsafe_b64encode(sha_digest).replace('=', '').replace('_', '-').lower()
+    sha_digest = hashlib.sha1((
+        '%s%s%s%s' % (
+            randrange(0, MAX_SESSION_KEY),
+            time.time(),
+            str(seed).encode('utf-8', 'replace'),
+            settings.SECRET_KEY
+        )
+    ).encode('utf-8')).digest()
+    return urlsafe_b64encode(sha_digest).decode('latin1').replace('=', '').replace('_', '-').lower()
 
 
 def split_tags(*tag_lists):
@@ -241,7 +246,7 @@ def truncate_html_words(s, num, end_text='...'):
 
     This is just a version of django.utils.text.truncate_html_words with no space before the end_text.
     """
-    s = force_unicode(s)
+    s = force_text(s)
     length = int(num)
     if length <= 0:
         return u''
index 1f2db19..5beb6dc 100644 (file)
@@ -171,10 +171,10 @@ def analyse_tags(request, tag_str):
             raise ResponseInstead(pdcounter_views.author_detail(request, chunks[1]))
         else:
             raise Http404
-    except Tag.MultipleObjectsReturned, e:
+    except Tag.MultipleObjectsReturned as e:
         # Ask the user to disambiguate
         raise ResponseInstead(differentiate_tags(request, e.tags, e.ambiguous_slugs))
-    except Tag.UrlDeprecationWarning, e:
+    except Tag.UrlDeprecationWarning as e:
         raise ResponseInstead(HttpResponsePermanentRedirect(
             reverse('tagged_object_list', args=['/'.join(tag.url_chunk for tag in e.tags)])))
 
index 37b9f60..a9f6e7f 100644 (file)
@@ -23,7 +23,7 @@ class Chunk(models.Model):
         verbose_name = _('chunk')
         verbose_name_plural = _('chunks')
 
-    def __unicode__(self):
+    def __str__(self):
         return self.key
 
     def save(self, *args, **kwargs):
@@ -45,5 +45,5 @@ class Attachment(models.Model):
         ordering = ('key',)
         verbose_name, verbose_name_plural = _('attachment'), _('attachments')
 
-    def __unicode__(self):
+    def __str__(self):
         return self.key
index bfa4330..76cc71e 100644 (file)
@@ -20,6 +20,7 @@ class ScheduleAdmin(admin.ModelAdmin):
     list_search = ['email']
     list_filter = ['is_active', 'is_cancelled']
     date_hierarchy = 'started_at'
+    raw_id_fields = ['membership']
     inlines = [PaymentInline]
 
 admin.site.register(models.Schedule, ScheduleAdmin)
@@ -32,7 +33,9 @@ admin.site.register(models.Payment, PaymentAdmin)
 
 
 class MembershipAdmin(admin.ModelAdmin):
-    pass
+    list_display = ['user']
+    raw_id_fields = ['user']
+    search_fields = ['user__username', 'user__email']
 
 admin.site.register(models.Membership, MembershipAdmin)
 
index adf8959..c5d5781 100644 (file)
@@ -1,7 +1,6 @@
 # -*- coding: utf-8
 from django import forms
 from . import models
-from . import widgets
 from .payment_methods import method_by_slug 
 
 
index a8b4089..1777174 100644 (file)
@@ -31,7 +31,7 @@ class Plan(models.Model):
         verbose_name = _('plan')
         verbose_name_plural = _('plans')
 
-    def __unicode__(self):
+    def __str__(self):
         return "%s %s" % (self.min_amount, self.get_interval_display())
     
     class Meta:
@@ -74,7 +74,7 @@ class Schedule(models.Model):
         verbose_name = _('schedule')
         verbose_name_plural = _('schedules')
 
-    def __unicode__(self):
+    def __str__(self):
         return self.key
 
     def save(self, *args, **kwargs):
@@ -109,7 +109,7 @@ class Payment(models.Model):
         verbose_name = _('payment')
         verbose_name_plural = _('payments')
 
-    def __unicode__(self):
+    def __str__(self):
         return "%s %s" % (self.schedule, self.payed_at)
 
 
@@ -122,8 +122,8 @@ class Membership(models.Model):
         verbose_name = _('membership')
         verbose_name_plural = _('memberships')
 
-    def __unicode__(self):
-        return u'tow. ' + unicode(self.user)
+    def __str__(self):
+        return u'tow. ' + str(self.user)
 
 
 class ReminderEmail(models.Model):
@@ -136,7 +136,7 @@ class ReminderEmail(models.Model):
         verbose_name_plural = _('reminder emails')
         ordering = ['days_before']
 
-    def __unicode__(self):
+    def __str__(self):
         if self.days_before >= 0:
             return ungettext('a day before expiration', '%d days before expiration', n=self.days_before)
         else:
index 4109509..a059433 100644 (file)
@@ -157,11 +157,11 @@ def extract_view(request, form_tag, extract_type_slug):
                 for key in keys:
                     if key not in record:
                         record[key] = ''
-                    if isinstance(record[key], basestring):
+                    if isinstance(record[key], str):
                         pass
                     elif isinstance(record[key], bool):
                         record[key] = 'tak' if record[key] else 'nie'
-                    elif isinstance(record[key], (list, tuple)) and all(isinstance(v, basestring) for v in record[key]):
+                    elif isinstance(record[key], (list, tuple)) and all(isinstance(v, str) for v in record[key]):
                         record[key] = ', '.join(record[key])
                     else:
                         record[key] = json.dumps(record[key])
index 0ab8201..e44bd9b 100644 (file)
@@ -2,7 +2,7 @@
 import yaml
 from hashlib import sha1
 from django.db import models
-from django.utils.encoding import smart_unicode
+from django.utils.encoding import smart_text
 from django.utils.translation import ugettext_lazy as _
 from jsonfield import JSONField
 from . import app_settings
@@ -20,7 +20,7 @@ class Contact(models.Model):
         if type(value) in (tuple, list, dict):
             value = yaml.safe_dump(value, allow_unicode=True, default_flow_style=False)
             if for_html:
-                value = smart_unicode(value).replace(u" ", unichr(160))
+                value = smart_text(value).replace(u" ", unichr(160))
         return value
 
     class Meta:
@@ -28,11 +28,11 @@ class Contact(models.Model):
         verbose_name = _('submitted form')
         verbose_name_plural = _('submitted forms')
 
-    def __unicode__(self):
-        return unicode(self.created_at)
+    def __str__(self):
+        return str(self.created_at)
 
     def digest(self):
-        serialized_body = ';'.join(sorted('%s:%s' % item for item in self.body.iteritems()))
+        serialized_body = ';'.join(sorted('%s:%s' % item for item in self.body.items()))
         data = '%s%s%s%s%s' % (self.id, self.contact, serialized_body, self.ip, self.form_tag)
         return sha1(data).hexdigest()
 
index 7ec0505..0f8aad9 100644 (file)
@@ -1,5 +1,4 @@
-# -*- coding: utf-8 -*-
-from urllib import unquote
+from urllib.parse import unquote
 
 from datetime import datetime
 from django.contrib.auth.decorators import permission_required
@@ -40,7 +39,7 @@ def form(request, form_tag, force_enabled=False):
     if request.method == 'POST':
         formsets = {
             prefix: formset_class(request.POST, request.FILES, prefix=prefix)
-            for prefix, formset_class in formset_classes.iteritems()}
+            for prefix, formset_class in formset_classes.items()}
         if form.is_valid() and all(formset.is_valid() for formset in formsets.itervalues()):
             contact = form.save(request, formsets.values())
             if form.result_page:
@@ -48,7 +47,7 @@ def form(request, form_tag, force_enabled=False):
             else:
                 return redirect('contact_thanks', form_tag)
     else:
-        formsets = {prefix: formset_class(prefix=prefix) for prefix, formset_class in formset_classes.iteritems()}
+        formsets = {prefix: formset_class(prefix=prefix) for prefix, formset_class in formset_classes.items()}
 
     return render(
         request, ['contact/%s/form.html' % form_tag, 'contact/form.html'],
index 7df6ddc..c8e086f 100644 (file)
@@ -19,7 +19,7 @@ class Qualifier(models.Model):
     class Meta:
         ordering = ['qualifier']
 
-    def __unicode__(self):
+    def __str__(self):
         return self.name or self.qualifier
 
 
index 342c963..f622374 100755 (executable)
@@ -1,17 +1,17 @@
-# -*- 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 optparse import make_option
 from django.core.management.base import BaseCommand
 
 
 class Command(BaseCommand):
-    option_list = BaseCommand.option_list + (
-        make_option('-q', '--quiet', action='store_false', dest='verbose', default=True, help='Suppress output'),
-    )
     help = 'Sends relevant funding notifications.'
 
+    def add_arguments(self, parser):
+        parser.add_argument(
+            '-q', '--quiet', action='store_false', dest='verbose',
+            default=True, help='Suppress output')
+
     def handle(self, **options):
 
         from datetime import date, timedelta
@@ -24,7 +24,7 @@ class Command(BaseCommand):
 
         for offer in Offer.past().filter(notified_end=None):
             if verbose:
-                print 'Notify end:', offer
+                print('Notify end:', offer)
             # The 'WL fund' list needs to be updated.
             caches[settings.CACHE_MIDDLEWARE_ALIAS].clear()
             offer.flush_includes()
@@ -35,5 +35,5 @@ class Command(BaseCommand):
                 current.end <= date.today() + timedelta(app_settings.DAYS_NEAR - 1) and
                 not current.notified_near):
             if verbose:
-                print 'Notify near:', current
+                print('Notify near:', current)
             current.notify_near()
index b126c76..6ca0261 100644 (file)
@@ -3,7 +3,7 @@
 # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
 #
 from datetime import date, datetime
-from urllib import urlencode
+from urllib.parse import urlencode
 from django.conf import settings
 from django.contrib.sites.models import Site
 from django.core.urlresolvers import reverse
@@ -48,7 +48,7 @@ class Offer(models.Model):
         verbose_name_plural = _('offers')
         ordering = ['-end']
 
-    def __unicode__(self):
+    def __str__(self):
         return u"%s - %s" % (self.author, self.title)
 
     def get_absolute_url(self):
@@ -218,7 +218,7 @@ class Perk(models.Model):
         verbose_name_plural = _('perks')
         ordering = ['-price']
 
-    def __unicode__(self):
+    def __str__(self):
         return "%s (%s%s)" % (self.name, self.price, u" for %s" % self.offer if self.offer else "")
 
 
@@ -248,8 +248,8 @@ class Funding(models.Model):
         """ QuerySet for all completed payments. """
         return cls.objects.exclude(payed_at=None)
 
-    def __unicode__(self):
-        return unicode(self.offer)
+    def __str__(self):
+        return str(self.offer)
 
     def get_absolute_url(self):
         return reverse('funding_funding', args=[self.pk])
@@ -318,8 +318,8 @@ class Spent(models.Model):
         verbose_name_plural = _('money spent on books')
         ordering = ['-timestamp']
 
-    def __unicode__(self):
-        return u"Spent: %s" % unicode(self.book)
+    def __str__(self):
+        return u"Spent: %s" % str(self.book)
 
 
 @receiver(getpaid.signals.new_payment_query)
index 5808336..e93d816 100644 (file)
@@ -10,8 +10,8 @@ from fnpdjango.utils.text import char_map
 # Punctuation is handled correctly and escaped as needed,
 # with the notable exception of backslash.
 sane_in_payu_title = re.escape(
-    string.uppercase +
-    string.lowercase +
+    string.ascii_uppercase +
+    string.ascii_lowercase +
     u'ąćęłńóśźżĄĆĘŁŃÓŚŹŻ' +
     string.digits +
     ' ' +
@@ -25,4 +25,4 @@ def replace_char(m):
 
 
 def sanitize_payment_title(value):
-    return re.sub('[^%s]' % sane_in_payu_title, replace_char, unicode(value))
+    return re.sub('[^%s]' % sane_in_payu_title, replace_char, str(value))
index c9c31e7..8999d99 100644 (file)
@@ -20,7 +20,7 @@ class InfoPage(models.Model):
         verbose_name = _('info page')
         verbose_name_plural = _('info pages')
 
-    def __unicode__(self):
+    def __str__(self):
         return self.title
 
     @models.permalink
index 2acc34a..47a72ba 100644 (file)
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 from datetime import date
-from urllib2 import urlopen
+from urllib.request import urlopen
 
 from django import forms
 from django.utils.translation import ugettext_lazy as _
index 19f3166..386dd1b 100644 (file)
@@ -113,7 +113,7 @@ class Command(BaseCommand):
         for record in ONIXRecord.objects.all():
             xml += self.render_product(record)
         xml += FOOTER
-        print xml.encode('utf-8')
+        print(xml.encode('utf-8'))
 
     def render_product(self, record):
         if record.product_form_detail:
index 796b7aa..85d54ec 100644 (file)
@@ -30,7 +30,7 @@ def parse_date(date_str):
 
 
 def get_descendants(element, tags):
-    if isinstance(tags, basestring):
+    if isinstance(tags, str):
         tags = [tags]
     return element.findall('.//' + '/'.join(ONIXNS(tag) for tag in tags))
 
@@ -44,14 +44,17 @@ def get_field(element, tags, allow_multiple=False):
 
 class Command(BaseCommand):
     help = "Import data from ONIX."
-    args = 'filename'
 
-    def handle(self, filename, *args, **options):
+    def add_arguments(self, parser):
+        parser.add_argument('filename')
+
+    def handle(self, **options):
+        filename = options['filename']
         tree = etree.parse(open(filename))
         for product in get_descendants(tree, 'Product'):
             isbn = get_field(product, ['ProductIdentifier', 'IDValue'])
             assert len(isbn) == 13
-            pool = ISBNPool.objects.get(prefix__in=[isbn[:i] for i in xrange(8, 11)])
+            pool = ISBNPool.objects.get(prefix__in=[isbn[:i] for i in range(8, 11)])
             contributors = [
                 self.parse_contributor(contributor)
                 for contributor in get_descendants(product, 'Contributor')]
@@ -62,7 +65,7 @@ class Command(BaseCommand):
                     get_field(product, ['PublishingDate', 'Date'], allow_multiple=True)),
                 'contributors': contributors,
             }
-            for field, tag in DIRECT_FIELDS.iteritems():
+            for field, tag in DIRECT_FIELDS.items():
                 record_data[field] = get_field(product, tag) or ''
             record = ONIXRecord.objects.create(**record_data)
             ONIXRecord.objects.filter(pk=record.pk).update(datestamp=parse_date(product.attrib['datestamp']))
@@ -78,7 +81,7 @@ class Command(BaseCommand):
         contributor_data = {
             'role': get_field(contributor, 'ContributorRole'),
         }
-        for key, value in data.iteritems():
+        for key, value in data.items():
             if value:
                 contributor_data[key] = value
         if contributor_data.get('name') == UNKNOWN:
index a4fa28a..ca8dbee 100644 (file)
@@ -22,7 +22,7 @@ class ISBNPool(models.Model):
     next_suffix = models.IntegerField()
     purpose = models.CharField(max_length=4, choices=PURPOSE_CHOICES)
 
-    def __unicode__(self):
+    def __str__(self):
         return self.prefix
 
     @classmethod
index 8cf7b5d..8073c75 100644 (file)
@@ -78,7 +78,7 @@ def get_format(record):
     if record.product_form_detail:
         return PRODUCT_FORMATS[record.product_form_detail][0]
     else:
-        return [key for key, value in PRODUCT_FORMS.iteritems() if value == record.product_form][0]
+        return [key for key, value in PRODUCT_FORMS.items() if value == record.product_form][0]
 
 
 @permission_required('add_onixrecord')
index a890f86..bcad601 100644 (file)
@@ -1,10 +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.
 #
 import re
-from cPickle import dump
-from optparse import make_option
+from pickle import dump
 
 from django.core.management.base import BaseCommand
 from django.core.management.color import color_style
@@ -17,15 +15,18 @@ re_text = re.compile(r'\n{3,}(.*?)\n*-----\n', re.S).search
 
 
 class Command(BaseCommand):
-    option_list = BaseCommand.option_list + (
-        make_option('-t', '--tags', dest='tags', metavar='SLUG,...',
-                    help='Use only books tagged with this tags'),
-        make_option('-i', '--include', dest='include', metavar='SLUG,...',
-                    help='Include specific books by slug'),
-        make_option('-e', '--exclude', dest='exclude', metavar='SLUG,...',
-                    help='Exclude specific books by slug')
-    )
-    help = 'Prepare data for Lesmianator.'
+    help = 'Prepare data for Leśmianator.'
+
+    def add_arguments(self, parser):
+        parser.add_argument(
+            '-t', '--tags', dest='tags', metavar='SLUG,...',
+            help='Use only books tagged with this tags')
+        parser.add_argument(
+            '-i', '--include', dest='include', metavar='SLUG,...',
+            help='Include specific books by slug')
+        parser.add_argument(
+            '-e', '--exclude', dest='exclude', metavar='SLUG,...',
+            help='Exclude specific books by slug')
 
     def handle(self, *args, **options):
         self.style = color_style()
@@ -37,7 +38,7 @@ class Command(BaseCommand):
         try:
             path = settings.LESMIANATOR_PICKLE
         except AttributeError:
-            print self.style.ERROR('LESMIANATOR_PICKLE not set in the settings.')
+            print(self.style.ERROR('LESMIANATOR_PICKLE not set in the settings.'))
             return
 
         books = []
@@ -59,22 +60,22 @@ class Command(BaseCommand):
         processed = skipped = 0
         for book in books:
             if verbose >= 2:
-                print 'Parsing', book.slug
+                print('Parsing', book.slug)
             if not book.txt_file:
                 if verbose >= 1:
-                    print self.style.NOTICE('%s has no TXT file' % book.slug)
+                    print(self.style.NOTICE('%s has no TXT file' % book.slug))
                 skipped += 1
                 continue
             f = open(book.txt_file.path)
             m = re_text(f.read())
             if not m:
-                print self.style.ERROR("Unknown text format: %s" % book.slug)
+                print(self.style.ERROR("Unknown text format: %s" % book.slug))
                 skipped += 1
                 continue
 
             processed += 1
             last_word = ''
-            text = unicode(m.group(1), 'utf-8').lower()
+            text = str(m.group(1), 'utf-8').lower()
             for letter in text:
                 mydict = lesmianator.setdefault(last_word, {})
                 mydict.setdefault(letter, 0)
@@ -84,18 +85,18 @@ class Command(BaseCommand):
 
         if not processed:
             if skipped:
-                print self.style.ERROR("No books with TXT files found")
+                print(self.style.ERROR("No books with TXT files found"))
             else:
-                print self.style.ERROR("No books found")
+                print(self.style.ERROR("No books found"))
             return
 
         try:
             dump(lesmianator, open(path, 'w'))
         except IOError:
-            print self.style.ERROR("Couldn't write to $s" % path)
+            print(self.style.ERROR("Couldn't write to $s" % path))
             return
 
         dump(lesmianator, open(path, 'w'))
         if verbose >= 1:
-            print "%d processed, %d skipped" % (processed, skipped)
-            print "Results dumped to %s" % path
+            print("%d processed, %d skipped" % (processed, skipped))
+            print("Results dumped to %s" % path)
index 684d1c4..0595810 100644 (file)
@@ -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.
 #
-import cPickle
-from cPickle import PickleError
+import pickle
+from pickle import PickleError
 from datetime import datetime
 from random import randint
-from StringIO import StringIO
 
 from django.core.files.base import ContentFile
 from django.db import models
@@ -33,7 +32,7 @@ class Poem(models.Model):
 
     try:
         f = open(settings.LESMIANATOR_PICKLE)
-        global_dictionary = cPickle.load(f)
+        global_dictionary = pickle.load(f)
         f.close()
     except (IOError, AttributeError, PickleError):
         global_dictionary = {}
@@ -43,7 +42,7 @@ class Poem(models.Model):
         self.seen_at = datetime.utcnow().replace(tzinfo=utc)
         self.save()
 
-    def __unicode__(self):
+    def __str__(self):
         return "%s (%s...)" % (self.slug, self.text[:20])
 
     @staticmethod
@@ -109,8 +108,8 @@ class Continuations(models.Model):
     class Meta:
         unique_together = (('content_type', 'object_id'), )
 
-    def __unicode__(self):
-        return "Continuations for: %s" % unicode(self.content_object)
+    def __str__(self):
+        return "Continuations for: %s" % str(self.content_object)
 
     @staticmethod
     def join_conts(a, b):
@@ -124,7 +123,6 @@ class Continuations(models.Model):
     @classmethod
     def for_book(cls, book, length=3):
         # count from this book only
-        output = StringIO()
         wldoc = book.wldocument(parse_dublincore=False)
         output = wldoc.as_text(('raw-text',)).get_bytes()
         del wldoc
@@ -158,7 +156,7 @@ class Continuations(models.Model):
             if not obj.pickle:
                 raise cls.DoesNotExist
             f = open(obj.pickle.path)
-            keys, conts = cPickle.load(f)
+            keys, conts = pickle.load(f)
             f.close()
             if set(keys) != should_keys:
                 raise cls.DoesNotExist
@@ -172,6 +170,6 @@ class Continuations(models.Model):
                 raise NotImplementedError('Lesmianator continuations: only Book and Tag supported')
 
             c, created = cls.objects.get_or_create(content_type=object_type, object_id=sth.id)
-            c.pickle.save(sth.slug+'.p', ContentFile(cPickle.dumps((should_keys, conts))))
+            c.pickle.save(sth.slug+'.p', ContentFile(pickle.dumps((should_keys, conts))))
             c.save()
             return conts
index 588620f..0ad0586 100644 (file)
@@ -16,7 +16,7 @@ class Catalog(models.Model):
         verbose_name = _('catalog')\r
         verbose_name_plural = _('catalogs')\r
 \r
-    def __unicode__(self):\r
+    def __str__(self):\r
         return self.name\r
 \r
     @models.permalink\r
@@ -37,7 +37,7 @@ class Library(models.Model):
         verbose_name = _('library')\r
         verbose_name_plural = _('libraries')\r
 \r
-    def __unicode__(self):\r
+    def __str__(self):\r
         return self.name\r
 \r
     @models.permalink\r
index 65fc435..0cbf867 100644 (file)
@@ -16,7 +16,7 @@ class Subscription(Model):
         verbose_name = _('subscription')
         verbose_name_plural = _('subscriptions')
 
-    def __unicode__(self):
+    def __str__(self):
         return self.email
 
     def hashcode(self):
index 0eeea8c..779406d 100644 (file)
@@ -116,7 +116,7 @@ class Catalogue(common.ResumptionOAIPMH):
     def identify(self, **kw):
         ident = common.Identify(
             'Wolne Lektury',  # generate
-            '%s/oaipmh' % unicode(WL_BASE),  # generate
+            '%s/oaipmh' % str(WL_BASE),  # generate
             '2.0',  # version
             [m[1] for m in settings.MANAGERS],  # adminEmails
             make_time_naive(self.earliest_datestamp),  # earliest datestamp of any change
index 004ee5b..1afbcda 100644 (file)
@@ -3,7 +3,7 @@
 # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
 #
 import os.path
-from urlparse import urljoin
+from urllib.parse import urljoin
 
 from django.contrib.syndication.views import Feed
 from django.core.urlresolvers import reverse
@@ -78,13 +78,13 @@ class OPDSFeed(Atom1Feed):
 
     _book_parent_img = lazy(lambda: full_url(os.path.join(settings.STATIC_URL, "img/book-parent.png")), str)()
     try:
-        _book_parent_img_size = unicode(os.path.getsize(os.path.join(settings.STATIC_ROOT, "img/book-parent.png")))
+        _book_parent_img_size = str(os.path.getsize(os.path.join(settings.STATIC_ROOT, "img/book-parent.png")))
     except OSError:
         _book_parent_img_size = ''
 
     _book_img = lazy(lambda: full_url(os.path.join(settings.STATIC_URL, "img/book.png")), str)()
     try:
-        _book_img_size = unicode(os.path.getsize(os.path.join(settings.STATIC_ROOT, "img/book.png")))
+        _book_img_size = str(os.path.getsize(os.path.join(settings.STATIC_ROOT, "img/book.png")))
     except OSError:
         _book_img_size = ''
 
index a7badbe..7de047a 100644 (file)
@@ -1,9 +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 django.contrib.auth.models import User
-from mock import MagicMock, Mock, patch, DEFAULT
+from unittest.mock import MagicMock, Mock, patch, DEFAULT
 from catalogue.test_utils import WLTestCase
 from .models import BillingAgreement, BillingPlan
 from .rest import user_is_subscribed
index c78a19a..c541ba8 100644 (file)
@@ -25,7 +25,7 @@ def paypal_form(request, app=False):
             try:
                 approval_url = agreement_approval_url(amount, app=app)
             except PaypalError as e:
-                return render(request, 'paypal/error_page.html', {'error': e.message})
+                return render(request, 'paypal/error_page.html', {'error': str(e)})
             return HttpResponseRedirect(approval_url)
     else:
         form = PaypalSubscriptionForm()
index 24866fe..4454e5a 100644 (file)
@@ -28,7 +28,7 @@ class Author(models.Model):
     def category(self):
         return "author"
 
-    def __unicode__(self):
+    def __str__(self):
         return self.name
 
     def __repr__(self):
@@ -71,7 +71,7 @@ class BookStub(models.Model):
         verbose_name = _('book stub')
         verbose_name_plural = _('book stubs')
 
-    def __unicode__(self):
+    def __str__(self):
         return self.title
 
     @permalink
index 677877e..7f8d346 100644 (file)
@@ -17,10 +17,10 @@ def rebuild_extra_info(apps, schema_editor):
         for field in areas_json[u'things'].values():
             field[u'object'] = field[u'object'].capitalize()
         pic.areas_json = areas_json
-        html_text = unicode(render_to_string('picture/picture_info.html', {
+        html_text = render_to_string('picture/picture_info.html', {
                     'things': pic.areas_json['things'],
                     'themes': pic.areas_json['themes'],
-                    }))
+                    })
         pic.html_file.save("%s.html" % pic.slug, ContentFile(html_text))
         pic.save()
 
index a3c098c..4d8dac8 100644 (file)
@@ -15,7 +15,7 @@ from ssify import flush_ssi_includes
 from catalogue.models.tag import prefetched_relations
 from catalogue.utils import split_tags
 from picture import tasks
-from StringIO import StringIO
+from io import BytesIO
 import jsonfield
 import itertools
 import logging
@@ -123,7 +123,7 @@ class Picture(models.Model):
 
         return ret
 
-    def __unicode__(self):
+    def __str__(self):
         return self.title
 
     def authors(self):
@@ -179,7 +179,7 @@ class Picture(models.Model):
         close_image_file = False
 
         if image_file is not None and not isinstance(image_file, File):
-            image_file = File(open(image_file))
+            image_file = File(open(image_file, 'rb'))
             close_image_file = True
 
         if not isinstance(xml_file, File):
@@ -197,7 +197,7 @@ class Picture(models.Model):
                 raise Picture.AlreadyExists('Picture %s already exists' % picture_xml.slug)
 
             picture.areas.all().delete()
-            picture.title = unicode(picture_xml.picture_info.title)
+            picture.title = str(picture_xml.picture_info.title)
             picture.extra_info = picture_xml.picture_info.to_dict()
 
             picture_tags = set(catalogue.models.Tag.tags_from_info(picture_xml.picture_info))
@@ -282,7 +282,7 @@ class Picture(models.Model):
 
             picture.width, picture.height = modified.size
 
-            modified_file = StringIO()
+            modified_file = BytesIO()
             modified.save(modified_file, format='JPEG', quality=95)
             # FIXME: hardcoded extension - detect from DC format or orginal filename
             picture.image_file.save(path.basename(picture_xml.image_path), File(modified_file))
@@ -372,6 +372,6 @@ class Picture(models.Model):
                 index.index_tags()
             if commit:
                 index.index.commit()
-        except Exception, e:
+        except Exception as e:
             index.index.rollback()
             raise e
index cae7db7..e80b0fc 100644 (file)
@@ -14,10 +14,10 @@ def generate_picture_html(picture_id):
     import picture.models
     pic = picture.models.Picture.objects.get(pk=picture_id)
 
-    html_text = unicode(render_to_string('picture/picture_info.html', {
+    html_text = render_to_string('picture/picture_info.html', {
                 'things': pic.areas_json['things'],
                 'themes': pic.areas_json['themes'],
-                }))
+                })
     pic.html_file.save("%s.html" % pic.slug, ContentFile(html_text))
 
 
@@ -26,7 +26,7 @@ def index_picture(picture_id, picture_info=None, **kwargs):
     from picture.models import Picture
     try:
         return Picture.objects.get(id=picture_id).search_index(picture_info, **kwargs)
-    except Exception, e:
-        print "Exception during index: %s" % e
+    except Exception as e:
+        print("Exception during index: %s" % e)
         print_exc()
         raise e
index a766b44..973d862 100644 (file)
@@ -57,7 +57,7 @@ def area_thumbnail_url(area, geometry):
             crop="%dpx %dpx %dpx %dpx" % tuple(map(lambda d: max(0, d), tuple(coords[0] + coords[1]))))
     except ZeroDivisionError:
         return ''
-    except Exception, e:
+    except Exception as e:
         logging.exception("Error creating a thumbnail for PictureArea")
         return ''
 
index a43c095..ecd5d66 100644 (file)
@@ -1,4 +1,3 @@
-# -*- 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.
 #
index bfc3639..4b1882d 100644 (file)
@@ -26,7 +26,7 @@ class Poll(models.Model):
             raise ValidationError(_('Slug of an open poll needs to be unique'))
         return super(Poll, self).clean()
 
-    def __unicode__(self):
+    def __str__(self):
         return self.question[:100] + ' (' + self.slug + ')'
 
     def get_absolute_url(self):
@@ -50,8 +50,8 @@ class PollItem(models.Model):
         verbose_name = _('vote item')
         verbose_name_plural = _('vote items')
 
-    def __unicode__(self):
-        return self.content + ' @ ' + unicode(self.poll)
+    def __str__(self):
+        return self.content + ' @ ' + str(self.poll)
 
     @property
     def vote_ratio(self):
index 79e5551..bb84b99 100644 (file)
@@ -16,5 +16,5 @@ class Notification(models.Model):
     class Meta:
         ordering = ['-timestamp']
 
-    def __unicode__(self):
+    def __str__(self):
         return '%s: %s' % (self.timestamp, self.title)
index f5d4c33..955f7d9 100755 (executable)
@@ -22,7 +22,7 @@ def render_to_pdf(output_path, template, context=None, add_files=None):
     :param dict add_files: a dictionary of additional files XeTeX will need
     """
 
-    from StringIO import StringIO
+    from io import BytesIO
     import shutil
     from tempfile import mkdtemp
     import subprocess
@@ -30,7 +30,7 @@ def render_to_pdf(output_path, template, context=None, add_files=None):
     from django.template.loader import render_to_string
 
     rendered = render_to_string(template, context)
-    texml = StringIO(rendered.encode('utf-8'))
+    texml = BytesIO(rendered.encode('utf-8'))
     tempdir = mkdtemp(prefix="render_to_pdf-")
     tex_path = os.path.join(tempdir, "doc.tex")
     with open(tex_path, 'w') as tex_file:
index da21e01..a944687 100644 (file)
@@ -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 sunburnt import sunburnt
+from scorched import connection
 from lxml import etree
-import urllib
+from urllib.parse import urlencode
 import warnings
-from sunburnt import search
+from scorched import search
 import copy
 from httplib2 import socket
 import re
@@ -25,7 +25,7 @@ class TermVectorOptions(search.Options):
     def update(self, positions=False, fields=None):
         if fields is None:
             fields = []
-        if isinstance(fields, basestring):
+        if isinstance(fields, (str, bytes)):
             fields = [fields]
         self.schema.check_fields(fields, {"stored": True})
         self.fields.update(fields)
@@ -42,13 +42,13 @@ class TermVectorOptions(search.Options):
         return opts
 
 
-class CustomSolrConnection(sunburnt.SolrConnection):
+class CustomSolrConnection(connection.SolrConnection):
     def __init__(self, *args, **kw):
         super(CustomSolrConnection, self).__init__(*args, **kw)
         self.analysis_url = self.url + "analysis/field/"
 
     def analyze(self, params):
-        qs = urllib.urlencode(params)
+        qs = urlencode(params)
         url = "%s?%s" % (self.analysis_url, qs)
         if len(url) > self.max_length_get_url:
             warnings.warn("Long query URL encountered - POSTing instead of GETting. "
@@ -63,11 +63,11 @@ class CustomSolrConnection(sunburnt.SolrConnection):
             kwargs = dict(method="GET")
         r, c = self.request(url, **kwargs)
         if r.status != 200:
-            raise sunburnt.SolrError(r, c)
+            raise connection.SolrError(r, c)
         return c
 
 
-# monkey patching sunburnt SolrSearch
+# monkey patching scorched SolrSearch
 search.SolrSearch.option_modules += ('term_vectorer',)
 
 
@@ -85,10 +85,10 @@ __original__init_common_modules = search.SolrSearch._init_common_modules
 setattr(search.SolrSearch, '_init_common_modules', __patched__init_common_modules)
 
 
-class CustomSolrInterface(sunburnt.SolrInterface):
+class CustomSolrInterface(connection.SolrInterface):
     # just copied from parent and SolrConnection -> CustomSolrConnection
     def __init__(self, url, schemadoc=None, http_connection=None, mode='', retry_timeout=-1,
-                 max_length_get_url=sunburnt.MAX_LENGTH_GET_URL):
+                 max_length_get_url=connection.MAX_LENGTH_GET_URL):
         self.conn = CustomSolrConnection(url, http_connection, retry_timeout, max_length_get_url)
         self.schemadoc = schemadoc
         if 'w' not in mode:
@@ -97,8 +97,8 @@ class CustomSolrInterface(sunburnt.SolrInterface):
             self.readable = False
         try:
             self.init_schema()
-        except socket.error, e:
-            raise socket.error, "Cannot connect to Solr server, and search indexing is enabled (%s)" % str(e)
+        except socket.error as e:
+            raise socket.error("Cannot connect to Solr server, and search indexing is enabled (%s)" % str(e))
 
     def _analyze(self, **kwargs):
         if not self.readable:
@@ -115,7 +115,7 @@ class CustomSolrInterface(sunburnt.SolrInterface):
         if 'query' in kwargs:
             args['q'] = kwargs['q']
 
-        params = map(lambda (k, v): (k.replace('_', '.'), v), sunburnt.params_from_dict(**args))
+        params = map(lambda k, v: (k.replace('_', '.'), v), connection.params_from_dict(**args))
 
         content = self.conn.analyze(params)
         doc = etree.fromstring(content)
@@ -139,7 +139,7 @@ class CustomSolrInterface(sunburnt.SolrInterface):
     def analyze(self, **kwargs):
         doc = self._analyze(**kwargs)
         terms = doc.xpath("//lst[@name='index']/arr[last()]/lst/str[1]")
-        terms = map(lambda n: unicode(n.text), terms)
+        terms = map(lambda n: str(n.text), terms)
         return terms
 
     def expand_margins(self, text, start, end):
index e2cfb54..c27cce1 100755 (executable)
@@ -4,7 +4,7 @@
 #
 from django import forms
 from django.forms.utils import flatatt
-from django.utils.encoding import smart_unicode
+from django.utils.encoding import smart_text
 from django.utils.safestring import mark_safe
 from json import dumps
 
@@ -21,7 +21,7 @@ class JQueryAutoCompleteWidget(forms.TextInput):
         final_attrs = self.build_attrs(self.attrs, attrs)
         final_attrs["name"] = name
         if value:
-            final_attrs['value'] = smart_unicode(value)
+            final_attrs['value'] = smart_text(value)
 
         if 'id' not in self.attrs:
             final_attrs['id'] = 'id_%s' % name
index d3377b1..f943a4d 100644 (file)
@@ -13,8 +13,8 @@ import catalogue.models
 import picture.models
 from pdcounter.models import Author as PDCounterAuthor, BookStub as PDCounterBook
 from itertools import chain
-import sunburnt
-import custom
+import scorched
+from . import custom
 import operator
 import logging
 from wolnelektury.utils import makedirs
@@ -129,7 +129,7 @@ class Index(SolrIndex):
         """
         uids = set()
         for q in queries:
-            if isinstance(q, sunburnt.search.LuceneQuery):
+            if isinstance(q, scorched.search.LuceneQuery):
                 q = self.index.query(q)
             q.field_limiter.update(['uid'])
             st = 0
@@ -324,9 +324,9 @@ class Index(SolrIndex):
                 elif type_indicator == dcparser.as_person:
                     p = getattr(book_info, field.name)
                     if isinstance(p, dcparser.Person):
-                        persons = unicode(p)
+                        persons = str(p)
                     else:
-                        persons = ', '.join(map(unicode, p))
+                        persons = ', '.join(map(str, p))
                     fields[field.name] = persons
                 elif type_indicator == dcparser.as_date:
                     dt = getattr(book_info, field.name)
@@ -478,7 +478,7 @@ class Index(SolrIndex):
                         fid = start.attrib['id'][1:]
                         handle_text.append(lambda text: None)
                         if start.text is not None:
-                            fragments[fid]['themes'] += map(unicode.strip, map(unicode, (start.text.split(','))))
+                            fragments[fid]['themes'] += map(str.strip, map(str, (start.text.split(','))))
                     elif end is not None and end.tag == 'motyw':
                         handle_text.pop()
 
@@ -610,14 +610,14 @@ class SearchResult(object):
         result._book = book
         return result
 
-    def __unicode__(self):
+    def __str__(self):
         return u"<SR id=%d %d(%d) hits score=%f %d snippets>" % \
             (self.book_id, len(self._hits),
              len(self._processed_hits) if self._processed_hits else -1,
              self._score, len(self.snippets))
 
-    def __str__(self):
-        return unicode(self).encode('utf-8')
+    def __bytes__(self):
+        return str(self).encode('utf-8')
 
     @property
     def score(self):
@@ -705,7 +705,7 @@ class SearchResult(object):
             if self.query_terms is not None:
                 for i in range(0, len(f[self.OTHER]['themes'])):
                     tms = f[self.OTHER]['themes'][i].split(r' +') + f[self.OTHER]['themes_pl'][i].split(' ')
-                    tms = map(unicode.lower, tms)
+                    tms = map(str.lower, tms)
                     for qt in self.query_terms:
                         if qt in tms:
                             themes_hit.add(f[self.OTHER]['themes'][i])
@@ -791,11 +791,11 @@ class PictureResult(object):
 
             self._hits.append(hit)
 
-    def __unicode__(self):
+    def __str__(self):
         return u"<PR id=%d score=%f >" % (self.picture_id, self._score)
 
     def __repr__(self):
-        return unicode(self)
+        return str(self)
 
     @property
     def score(self):
@@ -829,7 +829,7 @@ class PictureResult(object):
             if self.query_terms is not None:
                 for i in range(0, len(hit[self.OTHER]['themes'])):
                     tms = hit[self.OTHER]['themes'][i].split(r' +') + hit[self.OTHER]['themes_pl'][i].split(' ')
-                    tms = map(unicode.lower, tms)
+                    tms = map(str.lower, tms)
                     for qt in self.query_terms:
                         if qt in tms:
                             themes_hit.add(hit[self.OTHER]['themes'][i])
@@ -965,7 +965,7 @@ class Search(SolrIndex):
                         num -= 1
                 idx += 1
 
-        except IOError, e:
+        except IOError as e:
             book = catalogue.models.Book.objects.filter(id=book_id)
             if not book:
                 log.error("Book does not exist for book id = %d" % book_id)
index da4574f..c2fe78e 100755 (executable)
@@ -1,4 +1,3 @@
-# -*- 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.
 #
@@ -7,8 +6,6 @@ import traceback
 
 from django.core.management.base import BaseCommand
 
-from optparse import make_option
-
 
 def query_yes_no(question, default="yes"):
     """Ask a yes/no question via raw_input() and return their answer.
@@ -44,26 +41,31 @@ def query_yes_no(question, default="yes"):
 
 class Command(BaseCommand):
     help = 'Reindex everything.'
-    args = ''
-    
-    option_list = BaseCommand.option_list + (
-        make_option('-n', '--book-id', action='store_true', dest='book_id', default=False,
-                    help='book id instead of slugs'),
-        make_option('-t', '--just-tags', action='store_true', dest='just_tags', default=False,
-                    help='just reindex tags'),
-        make_option('--start', dest='start_from', default=None, help='start from this slug'),
-        make_option('--stop', dest='stop_after', default=None, help='stop after this slug'),
-    )
 
-    def handle(self, *args, **opts):
+    def add_arguments(self, parser):
+        parser.add_argument(
+                '-n', '--book-id', action='store_true', dest='book_id',
+                default=False, help='book id instead of slugs')
+        parser.add_argument(
+                '-t', '--just-tags', action='store_true', dest='just_tags',
+                default=False, help='just reindex tags')
+        parser.add_argument(
+                '--start', dest='start_from', default=None,
+                help='start from this slug')
+        parser.add_argument(
+                '--stop', dest='stop_after', default=None,
+                help='stop after this slug')
+        parser.add_argument('args', nargs='*', metavar='slug/id')
+
+    def handle(self, **opts):
         from catalogue.models import Book
         from search.index import Index
         idx = Index()
         
         if not opts['just_tags']:
-            if args:
+            if opts['args']:
                 books = []
-                for a in args:
+                for a in opts['args']:
                     if opts['book_id']:
                         books += Book.objects.filter(id=int(a)).all()
                     else:
@@ -83,7 +85,7 @@ class Command(BaseCommand):
                     if stop_after and slug > stop_after:
                         break
                     if not start_from or slug >= start_from:
-                        print b.slug
+                        print(b.slug)
                         idx.index_book(b)
                         idx.index.commit()
                     books.pop(0)
@@ -98,6 +100,6 @@ class Command(BaseCommand):
                     if not retry:
                         break
 
-        print 'Reindexing tags.'
+        print('Reindexing tags.')
         idx.index_tags()
         idx.index.commit()
index bb6b50f..8505189 100644 (file)
@@ -1,4 +1,3 @@
-# -*- 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.
 #
@@ -7,8 +6,6 @@ import traceback
 
 from django.core.management.base import BaseCommand
 
-from optparse import make_option
-
 
 def query_yes_no(question, default="yes"):
     """Ask a yes/no question via raw_input() and return their answer.
@@ -44,21 +41,21 @@ def query_yes_no(question, default="yes"):
 
 class Command(BaseCommand):
     help = 'Reindex pictures.'
-    args = ''
 
-    option_list = BaseCommand.option_list + (
-        make_option('-n', '--picture-id', action='store_true', dest='picture_id', default=False,
-                    help='picture id instead of slugs'),
-    )
+    def add_arguments(self, parser):
+        self.add_argument(
+                '-n', '--picture-id', action='store_true', dest='picture_id',
+                default=False, help='picture id instead of slugs')
+        self.add_argument('slug/id', nargs='*', metavar='slug/id')
 
-    def handle(self, *args, **opts):
+    def handle(self, **opts):
         from picture.models import Picture
         from search.index import Index
         idx = Index()
 
-        if args:
+        if opts['args']:
             pictures = []
-            for a in args:
+            for a in opts['args']:
                 if opts['picture_id']:
                     pictures += Picture.objects.filter(id=int(a)).all()
                 else:
@@ -68,7 +65,7 @@ class Command(BaseCommand):
         while pictures:
             try:
                 p = pictures[0]
-                print p.slug
+                print(p.slug)
                 idx.index_picture(p)
                 idx.index.commit()
                 pictures.pop(0)
index af80143..62512c9 100755 (executable)
@@ -1,26 +1,23 @@
-# -*- 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.core.management.base import BaseCommand
-
 from glob import glob
 from os import path
 from django.conf import settings
+from django.core.management.base import BaseCommand
 
 
 class Command(BaseCommand):
     help = 'Check snippets.'
-    args = ''
 
     def handle(self, *args, **opts):
         sfn = glob(settings.SEARCH_INDEX+'snippets/*')
         for fn in sfn:
-            print fn
+            print(fn)
             bkid = path.basename(fn)
             with open(fn) as f:
                 cont = f.read()
                 try:
                     cont.decode('utf-8')
                 except UnicodeDecodeError:
-                    print "error in snippets %s" % bkid
+                    print("error in snippets %s" % bkid)
index 344e79f..6c43040 100644 (file)
@@ -1,8 +1,7 @@
-# -*- 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 mock import Mock
+from unittest.mock import Mock
 from catalogue.models import Book, Tag
 from random import randint, choice
 
index d0cbb5c..0975c2a 100644 (file)
@@ -12,7 +12,7 @@ register = template.Library()
 def book_searched(context, result):
     # We don't need hits which lead to sections but do not have
     # snippets.
-    hits = filter(lambda (idx, h):
+    hits = filter(lambda idx, h:
                   result.snippets[idx] is not None or ('fragment' in h and h['themes_hit']),
                   enumerate(result.hits))
     # print "[tmpl: from %d hits selected %d]" % (len(result.hits), len(hits))
index 979c0d4..b9417ba 100644 (file)
@@ -37,7 +37,7 @@ class Cite(models.Model):
         verbose_name = _('cite')
         verbose_name_plural = _('cites')
 
-    def __unicode__(self):
+    def __str__(self):
         return u"%s: %s…" % (self.vip, self.text[:60])
 
     def get_absolute_url(self):
index ce61985..898d3ba 100755 (executable)
@@ -46,4 +46,4 @@ def book_shelf_tags(request, book_id):
             return ''
         ctx = {'tags': tags}
         return template.loader.render_to_string('social/shelf_tags.html', ctx)
-    return lazy(get_value, unicode)()
+    return lazy(get_value, str)()
index 61cb9d8..941f50f 100644 (file)
@@ -33,8 +33,8 @@ def sortify(value):
 
     """
 
-    if not isinstance(value, unicode):
-        value = unicode(value, 'utf-8')
+    if not isinstance(value, str):
+        value = str(value, 'utf-8')
 
     # try to replace chars
     value = re.sub('[^a-zA-Z0-9\\s\\-]', replace_char, value)
index 89b06ff..8b50552 100644 (file)
@@ -4,7 +4,7 @@
 #
 import json
 import time
-from StringIO import StringIO
+from io import BytesIO
 from django.db import models
 from django.utils.translation import ugettext_lazy as _
 from django.template.loader import render_to_string
@@ -24,7 +24,7 @@ class Sponsor(models.Model):
     logo = models.ImageField(_('logo'), upload_to='sponsorzy/sponsor/logo')
     url = models.URLField(_('url'), blank=True)
 
-    def __unicode__(self):
+    def __str__(self):
         return self.name
 
     def description(self):
@@ -75,7 +75,7 @@ class SponsorPage(models.Model):
                     (THUMB_WIDTH - simg.size[0]) / 2,
                     i * THUMB_HEIGHT + (THUMB_HEIGHT - simg.size[1]) / 2,
                     ))
-        imgstr = StringIO()
+        imgstr = BytesIO()
         sprite.save(imgstr, 'png')
 
         if self.sprite:
@@ -88,7 +88,7 @@ class SponsorPage(models.Model):
     html = property(fget=html)
 
     def save(self, *args, **kwargs):
-        if isinstance(self.sponsors, basestring):
+        if isinstance(self.sponsors, str):
             # Walkaround for weird jsonfield 'no-decode' optimization.
             self.sponsors = json.loads(self.sponsors)
         self.render_sprite()
@@ -103,5 +103,5 @@ class SponsorPage(models.Model):
     def flush_includes(self):
         flush_ssi_includes(['/sponsors/page/%s.html' % self.name])
 
-    def __unicode__(self):
+    def __str__(self):
         return self.name
index 7641549..06bc304 100644 (file)
@@ -23,7 +23,7 @@ class SponsorPageWidget(forms.Textarea):
 
     def render(self, name, value, attrs=None):
         output = [super(SponsorPageWidget, self).render(name, value, attrs)]
-        sponsors = [(unicode(obj), obj.pk, obj.logo.url) for obj in models.Sponsor.objects.all().iterator()]
+        sponsors = [(str(obj), obj.pk, obj.logo.url) for obj in models.Sponsor.objects.all().iterator()]
         sponsors_js = ', '.join('{name: "%s", id: %d, image: "%s"}' % sponsor for sponsor in sponsors)
         output.append(u'<script type="text/javascript">addEvent(window, "load", function(e) {')
         # TODO: "id_" is hard-coded here. This should instead use the correct
index dcd0a4b..cb2ec83 100644 (file)
@@ -4,9 +4,9 @@
 #
 from celery.task import task
 from django.conf import settings
-import httplib
+from http.client import HTTPConnection
 import logging
-import urlparse
+from urllib.parse import urlsplit
 
 logger = logging.getLogger(__name__)
 
@@ -15,7 +15,7 @@ PIWIK_API_VERSION = 1
 
 # Retrieve piwik information
 try:
-    _host = urlparse.urlsplit(settings.PIWIK_URL).netloc
+    _host = urlsplit(settings.PIWIK_URL).netloc
 except AttributeError:
     logger.debug("PIWIK_URL not configured.")
     _host = None
@@ -24,6 +24,6 @@ except AttributeError:
 @task(ignore_result=True)
 def track_request(piwik_args):
     piwik_url = "%s%s%s" % (settings.PIWIK_URL, u"/piwik.php?", piwik_args)
-    conn = httplib.HTTPConnection(_host)
+    conn = HTTPConnection(_host)
     conn.request('GET', piwik_url)
     conn.close()
index 86ce54c..eba7202 100644 (file)
@@ -10,7 +10,7 @@ import urllib
 from random import random
 from inspect import isclass
 
-from django.utils.encoding import force_str
+from django.utils.encoding import force_bytes
 
 from .tasks import track_request
 
@@ -21,10 +21,10 @@ def piwik_url(request):
     return urllib.urlencode(dict(
         idsite=getattr(settings, 'PIWIK_SITE_ID', '0'),
         rec=1,
-        url=force_str('http://%s%s' % (request.META['HTTP_HOST'], request.path)),
+        url=force_bytes('http://%s%s' % (request.META['HTTP_HOST'], request.path)),
         rand=int(random() * 0x10000),
         apiv=PIWIK_API_VERSION,
-        urlref=force_str(request.META.get('HTTP_REFERER', '')),
+        urlref=force_bytes(request.META.get('HTTP_REFERER', '')),
         ua=request.META.get('HTTP_USER_AGENT', ''),
         lang=request.META.get('HTTP_ACCEPT_LANGUAGE', ''),
         token_auth=getattr(settings, 'PIWIK_TOKEN', ''),
index b499ee8..b5fb06f 100644 (file)
@@ -22,8 +22,8 @@ class Suggestion(models.Model):
         verbose_name = _('suggestion')
         verbose_name_plural = _('suggestions')
 
-    def __unicode__(self):
-        return unicode(self.created_at)
+    def __str__(self):
+        return str(self.created_at)
 
 
 class PublishingSuggestion(models.Model):
@@ -69,5 +69,5 @@ class PublishingSuggestion(models.Model):
             spam = True
         return spam
 
-    def __unicode__(self):
-        return unicode(self.created_at)
+    def __str__(self):
+        return str(self.created_at)
index 25ff0c0..6d436a0 100644 (file)
@@ -9,7 +9,7 @@ from contact.forms import ContactForm
 from contact.fields import HeaderField
 from django import forms
 
-mark_safe_lazy = lazy(mark_safe, unicode)
+mark_safe_lazy = lazy(mark_safe, str)
 
 
 class KonkursForm(ContactForm):
index 263ee2c..ca7692e 100644 (file)
@@ -14,8 +14,8 @@ KEPT_FIELDS = {
 
 class Command(BaseCommand):
     def handle(self, *args, **options):
-        for provider, kept_fields in KEPT_FIELDS.iteritems():
+        for provider, kept_fields in KEPT_FIELDS.items():
             for sa in SocialAccount.objects.filter(provider=provider):
-                trimmed_data = {k: v for k, v in sa.extra_data.iteritems() if k in kept_fields}
+                trimmed_data = {k: v for k, v in sa.extra_data.items() if k in kept_fields}
                 sa.extra_data = trimmed_data
                 sa.save()
index 76ea473..d0e5d6c 100644 (file)
@@ -1,20 +1,17 @@
-# -*- 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 optparse import make_option
+import os
+import shutil
+import sys
+import tempfile
+import allauth
 from django.conf import settings
 from django.core.management.base import BaseCommand
 from django.core.management import call_command
-from .translation2po import get_languages
 from wolnelektury.utils import makedirs
+from .translation2po import get_languages
 
-import os
-import shutil
-import tempfile
-import sys
-
-import allauth
 
 ROOT = settings.ROOT_DIR
 
@@ -144,30 +141,42 @@ for appn in settings.INSTALLED_APPS:
     if is_our_app(app):
         try:
             SOURCES.append(AppLocale(app))
-        except LookupError, e:
-            print "no locales in %s" % app.__name__
+        except LookupError as e:
+            print("no locales in %s" % app.__name__)
 
 SOURCES.append(ModelTranslation('infopages', 'infopages_db'))
 SOURCES.append(CustomLocale(os.path.dirname(allauth.__file__), name='contrib'))
 
 
 class Command(BaseCommand):
-    option_list = BaseCommand.option_list + (
-        make_option('-l', '--load', help='load locales back to source', action='store_true', dest='load',
-                    default=False),
-        make_option('-c', '--compile', help='compile messages', action='store_true', dest='compile',
-                    default=False),
-        make_option('-g', '--generate', help='generate messages', action='store_true', dest='generate',
-                    default=False),
-        make_option('-L', '--lang', help='load just one language', dest='lang', default=None),
-        make_option('-d', '--directory', help='load from this directory', dest='directory', default=None),
-        make_option('-o', '--outfile', help='Resulting zip file', dest='outfile', default='./wl-locale.zip'),
-        make_option('-m', '--merge', help='Use git to merge. Please use with clean working directory.',
-                    action='store_true', dest='merge', default=False),
-        make_option('-M', '--message', help='commit message', dest='message', default='New locale'),
-    )
     help = 'Make a locale pack'
-    args = ''
+
+    def add_arguments(self, parser):
+        parser.add_argument(
+                '-l', '--load', help='load locales back to source',
+                action='store_true', dest='load', default=False)
+        parser.add_argument(
+                '-c', '--compile', help='compile messages',
+                action='store_true', dest='compile', default=False)
+        parser.add_argument(
+                '-g', '--generate', help='generate messages',
+                action='store_true', dest='generate', default=False)
+        parser.add_argument(
+                '-L', '--lang', help='load just one language',
+                dest='lang', default=None)
+        parser.add_argument(
+                '-d', '--directory', help='load from this directory',
+                dest='directory', default=None)
+        parser.add_argument(
+                '-o', '--outfile', help='Resulting zip file',
+                dest='outfile', default='./wl-locale.zip')
+        parser.add_argument(
+                '-m', '--merge', action='store_true',
+                dest='merge', default=False,
+                help='Use git to merge. Please use with clean working directory.')
+        parser.add_argument(
+                '-M', '--message', help='commit message',
+                dest='message', default='New locale')
 
     def current_rev(self):
         return os.popen('git rev-parse HEAD').read()
@@ -227,10 +236,10 @@ class Command(BaseCommand):
         for src in SOURCES:
             src.compile()
 
-    def handle(self, *a, **options):
+    def handle(self, **options):
         if options['load']:
             if not options['directory'] or not os.path.exists(options['directory']):
-                print "Directory not provided or does not exist, please use -d"
+                print("Directory not provided or does not exist, please use -d")
                 sys.exit(1)
 
             if options['merge']:
index 9fcb8bb..283470b 100644 (file)
@@ -1,17 +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.
 #
 import os
 import time
-from optparse import make_option
 from django.conf import settings
 from django.core.management.base import BaseCommand
 from django.db import models
-
 import polib
 from modeltranslation.translator import translator, NotRegistered
-
 from wolnelektury.utils import makedirs
 
 
@@ -48,18 +44,26 @@ def get_languages(langs):
 
 
 class Command(BaseCommand):
-    option_list = BaseCommand.option_list + (
-        make_option('-d', '--directory', help='Specify which directory should hold generated PO files',
-                    dest='directory', default=''),
-        make_option('-l', '--load', help='load locales back to source', action='store_true', dest='load',
-                    default=False),
-        make_option('-L', '--language', help='locales to load', dest='lang', default=None),
-        make_option('-n', '--poname', help='name of the po file [no extension]', dest='poname', default=None),
-        make_option('-k', '--keep-running', help='keep running even when missing an input file', dest='keep_running',
-                    default=False, action='store_true'),
-    )
     help = 'Export models from app to po files'
-    args = 'app'
+
+    def add_arguments(self, parser):
+        parser.add_argument(
+                '-d', '--directory', dest='directory', default='',
+                help='Specify which directory should hold generated PO files')
+        parser.add_argument(
+                '-l', '--load', help='load locales back to source',
+                action='store_true', dest='load', default=False)
+        parser.add_argument(
+                '-L', '--language', help='locales to load',
+                dest='lang', default=None)
+        parser.add_argument(
+                '-n', '--poname', help='name of the po file [no extension]',
+                dest='poname', default=None)
+        parser.add_argument(
+                '-k', '--keep-running', dest='keep_running',
+                help='keep running even when missing an input file',
+                default=False, action='store_true')
+        parser.add_argument('app')
 
     def get_models(self, app):
         for mdname in dir(app.models):
@@ -78,7 +82,8 @@ class Command(BaseCommand):
             else:
                 yield (md, opts)
 
-    def handle(self, appname, **options):
+    def handle(self, **options):
+        appname = options['app']
         if not options['poname']:
             options['poname'] = appname
         app = __import__(appname)
index 652c80c..956f979 100644 (file)
@@ -1,4 +1,3 @@
-# -*- coding: utf-8 -*-
 # Original version taken from http://www.djangosnippets.org/snippets/186/
 # Original author: udfalkso
 # Modified by: Shwagroo Team
@@ -9,7 +8,7 @@ import re
 import hotshot
 import hotshot.stats
 import tempfile
-import StringIO
+import io
 import pprint
 
 from django.conf import settings
@@ -101,7 +100,7 @@ class ProfileMiddleware(object):
         if (settings.DEBUG or request.user.is_superuser) and 'prof' in request.GET:
             self.prof.close()
 
-            out = StringIO.StringIO()
+            out = io.BytesIO()
             old_stdout = sys.stdout
             sys.stdout = out
 
index d13dfe1..3446695 100644 (file)
@@ -1,4 +1,3 @@
-# -*- 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.
 #
@@ -60,11 +59,7 @@ INSTALLED_APPS_CONTRIB = [
     'ssify',
     'django_extensions',
     'raven.contrib.django.raven_compat',
-
     'club.apps.ClubConfig',
-    'django_comments',
-    'django_comments_xtd',
-    'django_gravatar',
 
     # allauth stuff
     'allauth',
index 142d33f..c65a172 100644 (file)
@@ -1,18 +1,16 @@
-# -*- 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 codecs
 import csv
-import cStringIO
+from functools import wraps
+from inspect import getargspec
+from io import BytesIO
 import json
 import os
-from functools import wraps
-
 import pytz
-from inspect import getargspec
-
 import re
+
 from django.core.mail import send_mail
 from django.http import HttpResponse
 from django.template.loader import render_to_string
@@ -20,6 +18,7 @@ from django.utils import timezone
 from django.conf import settings
 from django.utils.translation import ugettext
 
+
 tz = pytz.timezone(settings.TIME_ZONE)
 
 
@@ -40,7 +39,7 @@ def makedirs(path):
 
 def stringify_keys(dictionary):
     return dict((keyword.encode('ascii'), value)
-                for keyword, value in dictionary.iteritems())
+                for keyword, value in dictionary.items())
 
 
 def json_encode(obj, sort_keys=True, ensure_ascii=False):
@@ -87,7 +86,7 @@ def ajax(login_required=False, method=None, template=None, permission_required=N
                 if request_params:
                     request_params = dict(
                         (key, json_decode_fallback(value))
-                        for key, value in request_params.iteritems()
+                        for key, value in request_params.items()
                         if fun_kwargs or key in fun_params)
                     kwargs.update(stringify_keys(request_params))
                 res = None
@@ -129,7 +128,7 @@ class UnicodeCSVWriter(object):
 
     def __init__(self, f, dialect=csv.excel, encoding="utf-8", **kwds):
         # Redirect output to a queue
-        self.queue = cStringIO.StringIO()
+        self.queue = BytesIO()
         self.writer = csv.writer(self.queue, dialect=dialect, **kwds)
         self.stream = f
         self.encoder = codecs.getincrementalencoder(encoding)()