Python 3
authorRadek Czajka <rczajka@rczajka.pl>
Mon, 11 Mar 2019 09:27:59 +0000 (10:27 +0100)
committerRadek Czajka <rczajka@rczajka.pl>
Mon, 11 Mar 2019 09:28:14 +0000 (10:28 +0100)
32 files changed:
src/catalogue/ebook_utils.py
src/catalogue/management/__init__.py
src/catalogue/management/commands/__init__.py
src/catalogue/management/commands/fixdc.py
src/catalogue/management/commands/import_wl.py
src/catalogue/management/commands/insert_isbn.py
src/catalogue/management/commands/mark_final.py
src/catalogue/management/commands/merge_books.py
src/catalogue/management/commands/prune_audience.py
src/catalogue/models/__init__.py
src/catalogue/models/book.py
src/catalogue/models/chunk.py
src/catalogue/models/image.py
src/catalogue/models/project.py
src/catalogue/test_utils.py
src/catalogue/views.py
src/catalogue/xml_tools.py
src/cover/forms.py
src/cover/management/commands/refresh_covers.py
src/cover/models.py
src/cover/utils.py
src/dvcs/models.py
src/dvcs/storage.py
src/email_mangler/templatetags/email.py
src/fileupload/views.py
src/toolbar/admin.py
src/toolbar/management/commands/fixbuttons.py
src/toolbar/models.py
src/wiki/helpers.py
src/wiki/models.py
src/wiki/views.py
src/wiki_img/views.py

index dae2e76..a61d18f 100644 (file)
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-from StringIO import StringIO
+from io import BytesIO
 from catalogue.models import Book
 from librarian import DocProvider
 from django.http import HttpResponse
@@ -12,7 +12,7 @@ class RedakcjaDocProvider(DocProvider):
         self.publishable = publishable
 
     def by_slug(self, slug):
-        return StringIO(Book.objects.get(dc_slug=slug
+        return BytesIO(Book.objects.get(dc_slug=slug
                     ).materialize(publishable=self.publishable
                     ).encode('utf-8'))
 
index e537f9e..c629abf 100644 (file)
@@ -54,14 +54,14 @@ class XmlUpdater(object):
     def fix_chunk(self, chunk, user, verbose=0, dry_run=False):
         """Runs the update for a single chunk."""
         if verbose >= 2:
-            print chunk.get_absolute_url()
+            print(chunk.get_absolute_url())
         old_head = chunk.head
         src = old_head.materialize()
         try:
             tree = etree.fromstring(src)
         except:
             if verbose:
-                print "%s: invalid XML" % chunk.get_absolute_url()
+                print("%s: invalid XML" % chunk.get_absolute_url())
             self.counters['Bad XML'] += 1
             return
 
@@ -82,7 +82,7 @@ class XmlUpdater(object):
 
         if not dry_run:
             new_head = chunk.commit(
-                etree.tostring(tree, encoding=unicode),
+                etree.tostring(tree, encoding='unicode'),
                 author=user,
                 description=self.commit_desc
             )
@@ -90,7 +90,7 @@ class XmlUpdater(object):
                 if old_head.publishable:
                     new_head.set_publishable(True)
         if verbose >= 2:
-            print "done"
+            print("done")
         self.counters['Updated chunks'] += 1
 
     def run(self, user, verbose=0, dry_run=False, books=None):
@@ -113,4 +113,4 @@ class XmlUpdater(object):
     def print_results(self):
         """Prints the counters."""
         for item in sorted(self.counters.items()):
-            print "%s: %d" % item
+            print("%s: %d" % item)
index e6f146f..bede078 100644 (file)
@@ -4,7 +4,6 @@
 # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
 #
 import sys
-from optparse import make_option
 from django.contrib.auth.models import User
 from django.core.management.base import BaseCommand
 from catalogue.models import Book
@@ -15,16 +14,19 @@ class XmlUpdaterCommand(BaseCommand):
 
     In a subclass, provide an XmlUpdater class in the `updater' attribute.
     """
-    option_list = BaseCommand.option_list + (
-        make_option('-q', '--quiet', action='store_false', dest='verbose',
-            default=True, help='Less output'),
-        make_option('-d', '--dry-run', action='store_true', dest='dry_run',
-            default=False, help="Don't actually touch anything"),
-        make_option('-u', '--username', dest='username', metavar='USER',
-            help='Assign commits to this user (required, preferably yourself).'),
-    )
     args = "[slug]..."
 
+    def add_arguments(self, parser):
+        parser.add_argument(
+            '-q', '--quiet', action='store_false', dest='verbose',
+            default=True, help='Less output')
+        parser.add_argument(
+            '-d', '--dry-run', action='store_true', dest='dry_run',
+            default=False, help="Don't actually touch anything")
+        parser.add_argument(
+            '-u', '--username', dest='username', metavar='USER',
+            help='Assign commits to this user (required, preferably yourself).')
+
     def handle(self, *args, **options):
         verbose = options.get('verbose')
         dry_run = options.get('dry_run')
@@ -33,7 +35,7 @@ class XmlUpdaterCommand(BaseCommand):
         if username:
             user = User.objects.get(username=username)
         else:
-            print 'Please provide a username.'
+            print('Please provide a username.')
             sys.exit(1)
 
         books = Book.objects.filter(slug__in=args) if args else None
index 3f997d0..0401e65 100644 (file)
@@ -18,7 +18,7 @@ class FixDC(XmlUpdater):
         try:
             WLURI.strict(elem.text)
         except ValidationError:
-            correct_field = unicode(WLURI.from_slug(
+            correct_field = str(WLURI.from_slug(
                                 WLURI(elem.text.strip()).slug))
             try:
                 WLURI.strict(correct_field)
@@ -26,9 +26,9 @@ class FixDC(XmlUpdater):
                 # Can't make a valid WLURI out of it, leave as is.
                 return False
             if verbose:
-                print "Changing %s from %s to %s" % (
+                print("Changing %s from %s to %s" % (
                         elem.tag, elem.text, correct_field
-                    )
+                    ))
             elem.text = correct_field
             return True
     for field in BookInfo.FIELDS:
@@ -42,9 +42,9 @@ class FixDC(XmlUpdater):
         current_about = elem.get(attr_name)
         if current_about != correct_about:
             if verbose:
-                print "Changing rdf:about from %s to %s" % (
+                print("Changing rdf:about from %s to %s" % (
                         current_about, correct_about
-                    )
+                    ))
             elem.set(attr_name, correct_about)
             return True
 
index 45c9e33..754474b 100644 (file)
@@ -2,8 +2,7 @@
 
 from collections import defaultdict
 import json
-from optparse import make_option
-import urllib2
+from urllib.request import urlopen
 
 from django.core.management.base import BaseCommand
 from django.core.management.color import color_style
@@ -18,12 +17,12 @@ WL_API = 'http://www.wolnelektury.pl/api/books/'
 
 
 class Command(BaseCommand):
-    option_list = BaseCommand.option_list + (
-        make_option('-q', '--quiet', action='store_false', dest='verbose', default=True,
-            help='Less output'),
-    )
     help = 'Imports XML files from WL.'
 
+    def add_arguments(self, parser):
+        parser.add_argument('-q', '--quiet', action='store_false', dest='verbose', default=True,
+            help='Less output')
+
     def handle(self, *args, **options):
 
         self.style = color_style()
@@ -34,11 +33,11 @@ class Command(BaseCommand):
         transaction.enter_transaction_management()
 
         if verbose:
-            print 'Reading currently managed files (skipping hidden ones).'
+            print('Reading currently managed files (skipping hidden ones).')
         slugs = defaultdict(list)
         for b in Book.objects.exclude(slug__startswith='.').all():
             if verbose:
-                print b.slug
+                print(b.slug)
             text = b.materialize().encode('utf-8')
             try:
                 info = BookInfo.from_bytes(text)
@@ -55,33 +54,33 @@ class Command(BaseCommand):
         }
 
         if verbose:
-            print 'Opening books list'
-        for book in json.load(urllib2.urlopen(WL_API)):
-            book_detail = json.load(urllib2.urlopen(book['href']))
-            xml_text = urllib2.urlopen(book_detail['xml']).read()
+            print('Opening books list')
+        for book in json.load(urlopen(WL_API)):
+            book_detail = json.load(urlopen(book['href']))
+            xml_text = urlopen(book_detail['xml']).read()
             info = BookInfo.from_bytes(xml_text)
             previous_books = slugs.get(info.slug)
             if previous_books:
                 if len(previous_books) > 1:
-                    print self.style.ERROR("There is more than one book "
-                        "with slug %s:")
+                    print(self.style.ERROR("There is more than one book "
+                        "with slug %s:") % info.slug)
                 previous_book = previous_books[0]
                 comm = previous_book.slug
             else:
                 previous_book = None
                 comm = '*'
-            print book_count, info.slug , '-->', comm
+            print(book_count, info.slug , '-->', comm)
             Book.import_xml_text(xml_text, title=info.title[:255],
                 slug=info.slug[:128], previous_book=previous_book,
                 commit_args=commit_args)
             book_count += 1
 
         # Print results
-        print
-        print "Results:"
-        print "Imported %d books from WL:" % (
-                book_count, )
-        print
+        print()
+        print("Results:")
+        print("Imported %d books from WL:" % (
+                book_count, ))
+        print()
 
 
         transaction.commit()
index 7548cb1..e342c90 100644 (file)
@@ -8,8 +8,6 @@ import csv
 import sys
 from django.contrib.auth.models import User
 from lxml import etree
-from optparse import make_option
-
 from collections import defaultdict
 from django.core.management import BaseCommand
 
@@ -42,24 +40,20 @@ def url_for_format(slug, format):
 
 
 class Command(BaseCommand):
-    option_list = BaseCommand.option_list + (
-        # make_option('-q', '--quiet', action='store_false', dest='verbose',
-        #     default=True, help='Less output'),
-        # make_option('-d', '--dry-run', action='store_true', dest='dry_run',
-        #     default=False, help="Don't actually touch anything"),
-        make_option(
-            '-u', '--username', dest='username', metavar='USER',
-            help='Assign commits to this user (required, preferably yourself).'),
-    )
     args = 'csv_file'
 
+    def add_arguments(self, parser):
+        self.add_argument(
+            '-u', '--username', dest='username', metavar='USER',
+            help='Assign commits to this user (required, preferably yourself).')
+
     def handle(self, csv_file, **options):
         username = options.get('username')
 
         if username:
             user = User.objects.get(username=username)
         else:
-            print 'Please provide a username.'
+            print('Please provide a username.')
             sys.exit(1)
 
         csvfile = open(csv_file, 'rb')
@@ -69,7 +63,7 @@ class Command(BaseCommand):
         csvfile.close()
 
         for slug, isbn_list in isbn_lists.iteritems():
-            print 'processing %s' % slug
+            print('processing %s' % slug)
             book = Book.objects.get(dc_slug=slug)
             chunk = book.chunk_set.first()
             old_head = chunk.head
@@ -77,7 +71,7 @@ class Command(BaseCommand):
             tree = etree.fromstring(src)
             isbn_node = tree.find('.//' + DCNS("relation.hasFormat"))
             if isbn_node is not None:
-                print '%s already contains ISBN metadata, skipping' % slug
+                print('%s already contains ISBN metadata, skipping' % slug)
                 continue
             desc = tree.find(".//" + RDFNS("Description"))
             for format, isbn in isbn_list:
@@ -92,12 +86,12 @@ class Command(BaseCommand):
                     element.tail = '\n'
                     desc.append(element)
             new_head = chunk.commit(
-                etree.tostring(tree, encoding=unicode),
+                etree.tostring(tree, encoding='unicode'),
                 author=user,
                 description='automatyczne dodanie isbn'
             )
-            print 'committed %s' % slug
+            print('committed %s' % slug)
             if old_head.publishable:
                 new_head.set_publishable(True)
             else:
-                print 'Warning: %s not publishable' % slug
+                print('Warning: %s not publishable' % slug)
index cdfaab9..11d63a3 100644 (file)
@@ -1,43 +1,35 @@
-# -*- coding: utf-8 -*-
-#
 # This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
 # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
 #
 import sys
 from django.contrib.auth.models import User
-from optparse import make_option
-
 from django.core.management import BaseCommand
 
 from catalogue.models import Book, Chunk
 
 
 class Command(BaseCommand):
-    option_list = BaseCommand.option_list + (
-        # make_option('-q', '--quiet', action='store_false', dest='verbose',
-        #     default=True, help='Less output'),
-        # make_option('-d', '--dry-run', action='store_true', dest='dry_run',
-        #     default=False, help="Don't actually touch anything"),
-        make_option(
-            '-u', '--username', dest='username', metavar='USER',
-            help='Assign commits to this user (required).'),
-    )
     args = 'slug_file'
 
+    def add_arguments(self, parser):
+        self.add_argument(
+            '-u', '--username', dest='username', metavar='USER',
+            help='Assign commits to this user (required).')
+
     def handle(self, slug_file, **options):
         username = options.get('username')
 
         if username:
             user = User.objects.get(username=username)
         else:
-            print 'Please provide a username.'
+            print('Please provide a username.')
             sys.exit(1)
 
         slugs = [line.strip() for line in open(slug_file)]
         books = Book.objects.filter(slug__in=slugs)
 
         for book in books:
-            print 'processing %s' % book.slug
+            print('processing %s' % book.slug)
             for chunk in book.chunk_set.all():
                 src = chunk.head.materialize()
                 chunk.commit(
@@ -47,4 +39,4 @@ class Command(BaseCommand):
                     tags=[Chunk.tag_model.objects.get(slug='editor-proofreading')],
                     publishable=True
                 )
-            print 'committed %s' % book.slug
+            print('committed %s' % book.slug)
index 82bd622..f6b37bb 100644 (file)
@@ -1,6 +1,3 @@
-# -*- coding: utf-8 -*-
-
-from optparse import make_option
 import sys
 
 from django.contrib.auth.models import User
@@ -24,23 +21,28 @@ def common_prefix(texts):
 
 
 class Command(BaseCommand):
-    option_list = BaseCommand.option_list + (
-        make_option('-s', '--slug', dest='new_slug', metavar='SLUG',
-            help='New slug of the merged book (defaults to common part of all slugs).'),
-        make_option('-t', '--title', dest='new_title', metavar='TITLE',
-            help='New title of the merged book (defaults to common part of all titles).'),
-        make_option('-q', '--quiet', action='store_false', dest='verbose', default=True,
-            help='Less output'),
-        make_option('-g', '--guess', action='store_true', dest='guess', default=False,
-            help='Try to guess what merges are needed (but do not apply them).'),
-        make_option('-d', '--dry-run', action='store_true', dest='dry_run', default=False,
-            help='Dry run: do not actually change anything.'),
-        make_option('-f', '--force', action='store_true', dest='force', default=False,
-            help='On slug conflict, hide the original book to archive.'),
-    )
     help = 'Merges multiple books into one.'
     args = '[slug]...'
 
+    def add_arguments(self, parser):
+        self.add_argument(
+            '-s', '--slug', dest='new_slug', metavar='SLUG',
+            help='New slug of the merged book (defaults to common part of all slugs).')
+        self.add_argument(
+            '-t', '--title', dest='new_title', metavar='TITLE',
+            help='New title of the merged book (defaults to common part of all titles).')
+        self.add_argument(
+            '-q', '--quiet', action='store_false', dest='verbose', default=True,
+            help='Less output')
+        self.add_argument(
+            '-g', '--guess', action='store_true', dest='guess', default=False,
+            help='Try to guess what merges are needed (but do not apply them).')
+        self.add_argument(
+            '-d', '--dry-run', action='store_true', dest='dry_run', default=False,
+            help='Dry run: do not actually change anything.')
+        self.add_argument(
+            '-f', '--force', action='store_true', dest='force', default=False,
+            help='On slug conflict, hide the original book to archive.')
 
     def print_guess(self, dry_run=True, force=False):
         from collections import defaultdict
@@ -49,9 +51,9 @@ class Command(BaseCommand):
     
         def read_slug(slug):
             res = []
-            res.append((re.compile(ur'__?(przedmowa)$'), -1))
-            res.append((re.compile(ur'__?(cz(esc)?|ksiega|rozdzial)__?(?P<n>\d*)$'), None))
-            res.append((re.compile(ur'__?(rozdzialy__?)?(?P<n>\d*)-'), None))
+            res.append((re.compile(r'__?(przedmowa)$'), -1))
+            res.append((re.compile(r'__?(cz(esc)?|ksiega|rozdzial)__?(?P<n>\d*)$'), None))
+            res.append((re.compile(r'__?(rozdzialy__?)?(?P<n>\d*)-'), None))
         
             for r, default in res:
                 m = r.search(slug)
@@ -87,20 +89,20 @@ class Command(BaseCommand):
                 conflicting_slugs.append(slug)
     
             title = file_to_title(slug)
-            print "./manage.py merge_books %s%s--title=%s --slug=%s \\\n    %s\n" % (
+            print("./manage.py merge_books %s%s--title=%s --slug=%s \\\n    %s\n" % (
                 '--dry-run ' if dry_run else '',
                 '--force ' if force else '',
                 quote(title), slug,
                 " \\\n    ".join(merge_slugs)
-                )
+                ))
     
         if conflicting_slugs:
             if force:
-                print self.style.NOTICE('# These books will be archived:')
+                print(self.style.NOTICE('# These books will be archived:'))
             else:
-                print self.style.ERROR('# ERROR: Conflicting slugs:')
+                print(self.style.ERROR('# ERROR: Conflicting slugs:'))
             for slug in conflicting_slugs:
-                print '#', slug
+                print('#', slug)
 
 
     def handle(self, *slugs, **options):
@@ -116,13 +118,13 @@ class Command(BaseCommand):
 
         if guess:
             if slugs:
-                print "Please specify either slugs, or --guess."
+                print("Please specify either slugs, or --guess.")
                 return
             else:
                 self.print_guess(dry_run, force)
                 return
         if not slugs:
-            print "Please specify some book slugs"
+            print("Please specify some book slugs")
             return
 
         # Start transaction management.
@@ -147,13 +149,13 @@ class Command(BaseCommand):
 
 
         if dry_run and verbose:
-            print self.style.NOTICE('DRY RUN: nothing will be changed.')
-            print
+            print(self.style.NOTICE('DRY RUN: nothing will be changed.'))
+            print()
 
         if verbose:
-            print "New title:", self.style.NOTICE(new_title)
-            print "New slug:", self.style.NOTICE(new_slug)
-            print
+            print("New title:", self.style.NOTICE(new_title))
+            print("New slug:", self.style.NOTICE(new_slug))
+            print()
 
         for i, book in enumerate(books):
             chunk_titles = []
@@ -172,12 +174,12 @@ class Command(BaseCommand):
                 chunk_slugs.append(new_chunk_slug)
 
                 if verbose:
-                    print "title: %s // %s  -->\n       %s // %s\nslug: %s / %s  -->\n      %s / %s" % (
+                    print("title: %s // %s  -->\n       %s // %s\nslug: %s / %s  -->\n      %s / %s" % (
                         book.title, chunk.title,
                         new_title, new_chunk_title,
                         book.slug, chunk.slug,
-                        new_slug, new_chunk_slug)
-                    print
+                        new_slug, new_chunk_slug))
+                    print()
 
             if not dry_run:
                 try:
@@ -193,9 +195,9 @@ class Command(BaseCommand):
                         # FIXME: there still may be a conflict
                         conflict.slug = '.' + conflict.slug
                         conflict.save()
-                        print self.style.NOTICE('Book with slug "%s" moved to "%s".' % (new_slug, conflict.slug))
+                        print(self.style.NOTICE('Book with slug "%s" moved to "%s".' % (new_slug, conflict.slug)))
                     else:
-                        print self.style.ERROR('ERROR: Book with slug "%s" exists.' % new_slug)
+                        print(self.style.ERROR('ERROR: Book with slug "%s" exists.' % new_slug))
                         return
 
                 if i:
index 114a26f..2f661c2 100644 (file)
@@ -7,7 +7,6 @@
 import sys
 from django.contrib.auth.models import User
 from lxml import etree
-from optparse import make_option
 
 from django.core.management import BaseCommand
 
@@ -16,24 +15,20 @@ from librarian import DCNS
 
 
 class Command(BaseCommand):
-    option_list = BaseCommand.option_list + (
-        # make_option('-q', '--quiet', action='store_false', dest='verbose',
-        #     default=True, help='Less output'),
-        # make_option('-d', '--dry-run', action='store_true', dest='dry_run',
-        #     default=False, help="Don't actually touch anything"),
-        make_option(
-            '-u', '--username', dest='username', metavar='USER',
-            help='Assign commits to this user (required, preferably yourself).'),
-    )
     args = 'exclude_file'
 
+    def add_arguments(self, parser):
+        parser.add_argument(
+            '-u', '--username', dest='username', metavar='USER',
+            help='Assign commits to this user (required, preferably yourself).')
+
     def handle(self, exclude_file, **options):
         username = options.get('username')
 
         if username:
             user = User.objects.get(username=username)
         else:
-            print 'Please provide a username.'
+            print('Please provide a username.')
             sys.exit(1)
 
         excluded_slugs = [line.strip() for line in open(exclude_file, 'rb') if line.strip()]
@@ -42,26 +37,26 @@ class Command(BaseCommand):
         for book in books:
             if not book.is_published():
                 continue
-            print 'processing %s' % book.slug
+            print('processing %s' % book.slug)
             chunk = book.chunk_set.first()
             old_head = chunk.head
             src = old_head.materialize()
             tree = etree.fromstring(src)
             audience_nodes = tree.findall('.//' + DCNS("audience"))
             if not audience_nodes:
-                print '%s has no audience, skipping' % book.slug
+                print('%s has no audience, skipping' % book.slug)
                 continue
 
             for node in audience_nodes:
                 node.getparent().remove(node)
 
             chunk.commit(
-                etree.tostring(tree, encoding=unicode),
+                etree.tostring(tree, encoding='unicode'),
                 author=user,
                 description='automatyczne skasowanie audience',
                 publishable=old_head.publishable
             )
-            print 'committed %s' % book.slug
+            print('committed %s' % book.slug)
             if not old_head.publishable:
-                print 'Warning: %s not publishable, last head: %s, %s' % (
-                    book.slug, old_head.author.username, old_head.description[:40].replace('\n', ' '))
+                print('Warning: %s not publishable, last head: %s, %s' % (
+                    book.slug, old_head.author.username, old_head.description[:40].replace('\n', ' ')))
index d0015c7..0b9c4d1 100755 (executable)
@@ -17,5 +17,5 @@ class User(AuthUser):
     class Meta:
         proxy = True
 
-    def __unicode__(self):
+    def __str__(self):
         return "%s %s" % (self.first_name, self.last_name)
index 0ed8958..229b3d0 100755 (executable)
@@ -66,13 +66,13 @@ class Book(models.Model):
     def __len__(self):
         return self.chunk_set.count()
 
-    def __nonzero__(self):
+    def __bool__(self):
         """
             Necessary so that __len__ isn't used for bool evaluation.
         """
         return True
 
-    def __unicode__(self):
+    def __str__(self):
         return self.title
 
     @models.permalink
@@ -266,12 +266,12 @@ class Book(models.Model):
 
         try:
             bi = self.wldocument(changes=changes, strict=True).book_info
-        except ParseError, e:
-            raise AssertionError(_('Invalid XML') + ': ' + unicode(e))
+        except ParseError as e:
+            raise AssertionError(_('Invalid XML') + ': ' + str(e))
         except NoDublinCore:
             raise AssertionError(_('No Dublin Core found.'))
-        except ValidationError, e:
-            raise AssertionError(_('Invalid Dublin Core') + ': ' + unicode(e))
+        except ValidationError as e:
+            raise AssertionError(_('Invalid Dublin Core') + ': ' + str(e))
 
         valid_about = self.correct_about()
         assert bi.about == valid_about, _("rdf:about is not") + " " + valid_about
@@ -279,7 +279,7 @@ class Book(models.Model):
     def publishable_error(self):
         try:
             return self.assert_publishable()
-        except AssertionError, e:
+        except AssertionError as e:
             return e
         else:
             return None
index ade5cde..4182d08 100755 (executable)
@@ -43,7 +43,7 @@ class Chunk(dvcs_models.Document):
     # Representing
     # ============
 
-    def __unicode__(self):
+    def __str__(self):
         return "%d:%d: %s" % (self.book_id, self.number, self.title)
 
     @models.permalink
@@ -54,7 +54,7 @@ class Chunk(dvcs_models.Document):
         title = self.book.title
         if self.title:
             title += ", %s" % self.title
-        if book_length > 1:
+        if book_length and book_length > 1:
             title += " (%d/%d)" % (self.number, book_length)
         return title
 
index 34f61f9..8cadcec 100755 (executable)
@@ -38,7 +38,7 @@ class Image(dvcs_models.Document):
     # Representing
     # ============
 
-    def __unicode__(self):
+    def __str__(self):
         return self.title
 
     @models.permalink
@@ -81,11 +81,11 @@ class Image(dvcs_models.Document):
             picture = WLPicture.from_bytes(
                     picture_xml.encode('utf-8'),
                     image_store=SelfImageStore)
-        except ParseError, e:
+        except ParseError as e:
             raise AssertionError(_('Invalid XML') + ': ' + str(e))
         except NoDublinCore:
             raise AssertionError(_('No Dublin Core found.'))
-        except ValidationError, e:
+        except ValidationError as e:
             raise AssertionError(_('Invalid Dublin Core') + ': ' + str(e))
 
         valid_about = self.correct_about()
@@ -95,7 +95,7 @@ class Image(dvcs_models.Document):
     def publishable_error(self):
         try:
             return self.assert_publishable()
-        except AssertionError, e:
+        except AssertionError as e:
             return e
         else:
             return None
index eb95102..d7cc0df 100755 (executable)
@@ -19,5 +19,5 @@ class Project(models.Model):
         verbose_name = _('project')
         verbose_name_plural = _('projects')
 
-    def __unicode__(self):
+    def __str__(self):
         return self.name
index 2b08545..cacc9c8 100644 (file)
@@ -11,4 +11,4 @@ from os.path import abspath, dirname, join
 def get_fixture(path):
     f_path = join(dirname(abspath(__file__)), 'tests/files', path)
     with open(f_path) as f:
-        return unicode(f.read(), 'utf-8')
+        return f.read()
index 5551a5c..1497be0 100644 (file)
@@ -3,9 +3,7 @@ from collections import defaultdict
 from datetime import datetime, date, timedelta
 import logging
 import os
-from StringIO import StringIO
-from urllib import unquote
-from urlparse import urlsplit, urlunsplit
+from urllib.parse import unquote, urlsplit, urlunsplit
 
 from django.conf import settings
 from django.contrib import auth
@@ -470,7 +468,7 @@ def chunk_mass_edit(request):
     if stage:
         try:
             stage = Chunk.tag_model.objects.get(slug=stage)
-        except Chunk.DoesNotExist, e:
+        except Chunk.DoesNotExist as e:
             stage = None
        
         for c in chunks: c.stage = stage
@@ -481,7 +479,7 @@ def chunk_mass_edit(request):
     if username:
         try:
             user = User.objects.get(username=username)
-        except User.DoesNotExist, e:
+        except User.DoesNotExist as e:
             user = None
             
         for c in chunks: c.user = user
@@ -490,7 +488,7 @@ def chunk_mass_edit(request):
     if project_id:
         try:
             project = Project.objects.get(pk=int(project_id))
-        except (Project.DoesNotExist, ValueError), e:
+        except (Project.DoesNotExist, ValueError) as e:
             project = None
         for c in chunks:
             book = c.book
@@ -513,7 +511,7 @@ def image_mass_edit(request):
     if stage:
         try:
             stage = Image.tag_model.objects.get(slug=stage)
-        except Image.DoesNotExist, e:
+        except Image.DoesNotExist as e:
             stage = None
        
         for c in images: c.stage = stage
@@ -524,7 +522,7 @@ def image_mass_edit(request):
     if username:
         try:
             user = User.objects.get(username=username)
-        except User.DoesNotExist, e:
+        except User.DoesNotExist as e:
             user = None
             
         for c in images: c.user = user
@@ -533,7 +531,7 @@ def image_mass_edit(request):
     if project_id:
         try:
             project = Project.objects.get(pk=int(project_id))
-        except (Project.DoesNotExist, ValueError), e:
+        except (Project.DoesNotExist, ValueError) as e:
             project = None
         for c in images:
             c.project = project
@@ -584,7 +582,7 @@ def publish(request, slug):
         book.publish(request.user, host=protocol + request.get_host(), days=days, beta=beta)
     except NotAuthorizedError:
         return http.HttpResponseRedirect(reverse('apiclient_oauth' if not beta else 'apiclient_beta_oauth'))
-    except BaseException, e:
+    except BaseException as e:
         return http.HttpResponse(repr(e))
     else:
         return http.HttpResponseRedirect(book.get_absolute_url())
@@ -601,7 +599,7 @@ def publish_image(request, slug):
         image.publish(request.user)
     except NotAuthorizedError:
         return http.HttpResponseRedirect(reverse('apiclient_oauth'))
-    except BaseException, e:
+    except BaseException as e:
         return http.HttpResponse(e)
     else:
         return http.HttpResponseRedirect(image.get_absolute_url())
index 242714b..ff7b031 100644 (file)
@@ -60,7 +60,7 @@ def add_trim_begin(text):
     master.insert(0, trim_tag)
     trim_tag.tail = '\n\n\n' + (master.text or '')
     master.text = '\n'
-    return unicode(etree.tostring(e, encoding="utf-8"), 'utf-8')
+    return str(etree.tostring(e, encoding="utf-8"), 'utf-8')
 
 
 def add_trim_end(text):
@@ -79,7 +79,7 @@ def add_trim_end(text):
         prev.tail = (prev.tail or '') + '\n\n\n'
     else:
         master.text = (master.text or '') + '\n\n\n'
-    return unicode(etree.tostring(e, encoding="utf-8"), 'utf-8')
+    return str(etree.tostring(e, encoding="utf-8"), 'utf-8')
 
 
 def split_xml(text):
@@ -124,13 +124,13 @@ def split_xml(text):
                 del parent[0]
             element, parent = parent, parent.getparent()
         chunks[:0] = [[name,
-            unicode(etree.tostring(copied, encoding='utf-8'), 'utf-8')
+            str(etree.tostring(copied, encoding='utf-8'), 'utf-8')
             ]]
 
         parts = src.findall('.//naglowek_rozdzial')
 
     chunks[:0] = [[u'początek',
-        unicode(etree.tostring(src, encoding='utf-8'), 'utf-8')
+        str(etree.tostring(src, encoding='utf-8'), 'utf-8')
         ]]
 
     for ch in chunks[1:]:
index e6f7868..e5bc400 100755 (executable)
@@ -3,7 +3,7 @@
 # This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
 # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
 #
-from StringIO import StringIO
+from io import BytesIO
 
 from django import forms
 from django.conf import settings
@@ -55,7 +55,7 @@ class ImageAddForm(forms.ModelForm):
             raise forms.ValidationError(ugettext('No image specified'))
         if download_url:
             image_data = URLOpener().open(download_url).read()
-            width, height = PILImage.open(StringIO(image_data)).size
+            width, height = PILImage.open(BytesIO(image_data)).size
         else:
             width, height = PILImage.open(uploaded_file.file).size
         min_width, min_height = settings.MIN_COVER_SIZE
index cc0ef31..0e998c9 100644 (file)
@@ -4,8 +4,6 @@
 # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
 #
 import urllib2 as urllib
-from optparse import make_option
-
 from django.core.files.base import ContentFile
 from django.core.management import BaseCommand
 
@@ -14,34 +12,33 @@ from cover.utils import get_flickr_data, URLOpener, FlickrError
 
 
 class Command(BaseCommand):
-    option_list = BaseCommand.option_list + (
-        make_option('--from', dest='from_id', type=int, default=1),
-    )
+    def add_arguments(self, parser):
+        parser.add_argument('--from', dest='from_id', type=int, default=1)
 
     def handle(self, *args, **options):
         from_id = options.get('from_id', 1)
         images = Image.objects.filter(id__gte=from_id).exclude(book=None).order_by('id')
         images = images.filter(source_url__contains='flickr.com').exclude(download_url__endswith='_o.jpg')
         for image in images:
-            print image.id
+            print(image.id)
             try:
                 flickr_data = get_flickr_data(image.source_url)
-                print flickr_data
+                print(flickr_data)
             except FlickrError as e:
-                print 'Flickr analysis failed: %s' % e
+                print('Flickr analysis failed: %s' % e)
             else:
                 flickr_url = flickr_data['download_url']
                 if flickr_url != image.download_url:
                     same_url = Image.objects.filter(download_url=flickr_url)
                     if same_url:
-                        print 'Download url already present in image %s' % same_url.get().id
+                        print('Download url already present in image %s' % same_url.get().id)
                         continue
                 try:
                     t = URLOpener().open(flickr_url).read()
                 except urllib.URLError:
-                    print 'Broken download url'
+                    print('Broken download url')
                 except IOError:
-                    print 'Connection failed'
+                    print('Connection failed')
                 else:
                     image.download_url = flickr_url
                     image.file.save(image.file.name, ContentFile(t))
index d83dad3..ea69f65 100644 (file)
@@ -34,7 +34,7 @@ class Image(models.Model):
         verbose_name = _('cover image')
         verbose_name_plural = _('cover images')
 
-    def __unicode__(self):
+    def __str__(self):
         return u"%s - %s" % (self.author, self.title)
 
     @models.permalink
index 51aee19..75bf96d 100755 (executable)
@@ -5,7 +5,7 @@
 #
 import json
 import re
-from urllib import FancyURLopener
+from urllib.request import FancyURLopener
 
 from django.contrib.sites.models import Site
 
index 270f24e..d6b0cc6 100644 (file)
@@ -30,7 +30,7 @@ class Tag(models.Model):
         abstract = True
         ordering = ['ordering']
 
-    def __unicode__(self):
+    def __str__(self):
         return self.name
 
     @classmethod
@@ -93,7 +93,7 @@ class Change(models.Model):
         ordering = ('created_at',)
         unique_together = ['tree', 'revision']
 
-    def __unicode__(self):
+    def __str__(self):
         return u"Id: %r, Tree %r, Parent %r, Data: %s" % (self.id, self.tree_id, self.parent_id, self.data)
 
     def author_str(self):
@@ -124,7 +124,7 @@ class Change(models.Model):
         f = self.data.storage.open(self.data)
         text = f.read()
         f.close()
-        return unicode(text, 'utf-8')
+        return str(text, 'utf-8')
 
     def merge_with(self, other, author=None, 
             author_name=None, author_email=None, 
@@ -225,11 +225,9 @@ class DocumentMeta(ModelBase):
         return model
 
 
-class Document(models.Model):
+class Document(models.Model, metaclass=DocumentMeta):
     """File in repository. Subclass it to use version control in your app."""
 
-    __metaclass__ = DocumentMeta
-
     # default repository path
     REPO_PATH = os.path.join(settings.MEDIA_ROOT, 'dvcs')
 
@@ -238,7 +236,7 @@ class Document(models.Model):
     class Meta:
         abstract = True
 
-    def __unicode__(self):
+    def __str__(self):
         return u"{0}, HEAD: {1}".format(self.id, self.head_id)
 
     def materialize(self, change=None):
@@ -256,12 +254,12 @@ class Document(models.Model):
         This will automatically merge the commit into the main branch,
         if parent is not document's head.
 
-        :param unicode text: new version of the document
+        :param str text: new version of the document
         :param parent: parent revision (head, if not specified)
         :type parent: Change or None
         :param User author: the commiter
-        :param unicode author_name: commiter name (if ``author`` not specified)
-        :param unicode author_email: commiter e-mail (if ``author`` not specified)
+        :param str author_name: commiter name (if ``author`` not specified)
+        :param str author_email: commiter e-mail (if ``author`` not specified)
         :param Tag[] tags: list of tags to apply to the new commit
         :param bool publishable: set new commit as ready to publish
         :returns: new head
index 91f78e6..a57d759 100755 (executable)
@@ -3,6 +3,7 @@ from zlib import compress, decompress
 from django.core.files.base import ContentFile, File
 from django.core.files.storage import FileSystemStorage
 from django.utils.deconstruct import deconstructible
+from django.utils.encoding import force_bytes
 
 
 @deconstructible
@@ -15,6 +16,7 @@ class GzipFileSystemStorage(FileSystemStorage):
         return ContentFile(decompress(text))
 
     def _save(self, name, content):
-        content = ContentFile(compress(content.read()))
+        data = force_bytes(content.read())
+        content = ContentFile(compress(data))
 
         return super(GzipFileSystemStorage, self)._save(name, content)
index 376117a..40ad72f 100755 (executable)
@@ -1,3 +1,4 @@
+import codecs
 from django.utils.html import escape
 from django.utils.safestring import mark_safe
 from django.utils.translation import ugettext as _
@@ -19,7 +20,7 @@ def email_link(email):
     mangled = "%s %s %s" % (name, at, (' %s ' % dot).join(domain.split('.')))
     return mark_safe("<a class='mangled' data-addr1='%(name)s' "
         "data-addr2='%(domain)s'>%(mangled)s</a>" % {
-            'name': name.encode('rot13'),
-            'domain': domain.encode('rot13'),
+            'name': codecs.encode(name, 'rot13'),
+            'domain': codecs.encode(domain, 'rot13'),
             'mangled': mangled,
         })
index a2025fe..9993ddb 100644 (file)
@@ -1,6 +1,6 @@
 import json
 import os
-from urllib import quote
+from urllib.parse import quote
 from django.conf import settings
 from django.http import HttpResponse, HttpResponseRedirect, HttpResponseForbidden, Http404
 from django.utils.decorators import method_decorator
index 654480c..9d3c68d 100644 (file)
@@ -15,7 +15,7 @@ class ButtonAdminForm(forms.ModelForm):
         value = self.cleaned_data['params']
         try:
             return json.dumps(json.loads(value))
-        except ValueError, e:
+        except ValueError as e:
             raise forms.ValidationError(e)
 
 
index de48ced..7fed15a 100644 (file)
@@ -15,29 +15,29 @@ class Command(NoArgsCommand):
 
     def handle_noargs(self, **options):
         buttons = Button.objects.all()
-        print "Validating parameters... "
+        print("Validating parameters... ")
         for b in buttons:
             params = b.params
             try:
                 v = json.loads(b.params)
-            except ValueError, e:
-                print 'Trying to fix button "%s" ...' % b.slug
+            except ValueError as e:
+                print('Trying to fix button "%s" ...' % b.slug)
                 if params[0] == u'(':
                     params = params[1:]
                 if params[-1] == u')':
                     params = params[:-1]
                 try:
                     v = son.loads(re.sub(u'([\\w-]+)\\s*:', u'"\\1": ', params).encode('utf-8'))
-                except ValueError, e:
-                    print "Unable to fix '%s' " % b.params
-                    print "Try to fix this button manually and rerun the script."
+                except ValueError as e:
+                    print("Unable to fix '%s' " % b.params)
+                    print("Try to fix this button manually and rerun the script.")
                     return False
 
             # resave
             b.params = json.dumps(v)
             b.save()
 
-        print "Merge duplicate buttons (if any)..."
+        print("Merge duplicate buttons (if any)...")
         hash = {}
         for b in buttons:
             if b.slug not in hash:
@@ -45,13 +45,13 @@ class Command(NoArgsCommand):
                 continue
 
             # duplicate button
-            print "Found duplicate of '%s'" % b.slug
+            print("Found duplicate of '%s'" % b.slug)
             a = hash[b.slug]
 
             remove_duplicate = True
             if a.params != b.params:
-                print "Conflicting params for duplicate of '%s'." % b.slug
-                print "Groups will be joined, but won't remove duplicates."
+                print("Conflicting params for duplicate of '%s'." % b.slug)
+                print("Groups will be joined, but won't remove duplicates.")
                 remove_duplicate = False
 
             for g in b.group.all():
@@ -63,11 +63,11 @@ class Command(NoArgsCommand):
             if remove_duplicate:
                 b.delete()
 
-        print "Searching for empty groups and orphaned buttons:"
+        print("Searching for empty groups and orphaned buttons:")
         for g in ButtonGroup.objects.all():
             if len(g.button_set.all()) == 0:
-                print "Empty group: '%s'" % g.slug
+                print("Empty group: '%s'" % g.slug)
 
         for b in Button.objects.all():
             if len(b.group.all()) == 0:
-                print "orphan: '%s'" % b.slug
+                print("orphan: '%s'" % b.slug)
index a23e346..fbaa396 100644 (file)
@@ -16,7 +16,7 @@ class ButtonGroup(models.Model):
         ordering = ('position', 'name',)
         verbose_name, verbose_name_plural = _('button group'), _('button groups')
 
-    def __unicode__(self):
+    def __str__(self):
         return self.name
 
     def to_dict(self, with_buttons=False):
@@ -63,7 +63,7 @@ class Button(models.Model):
             'scriptlet_id': self.scriptlet_id,
         }
 
-    def __unicode__(self):
+    def __str__(self):
         return self.label
 
 
@@ -71,5 +71,5 @@ class Scriptlet(models.Model):
     name = models.CharField(max_length=64, primary_key=True)
     code = models.TextField()
 
-    def __unicode__(self):
+    def __str__(self):
         return _(u'javascript') + u':' + self.name
index 877a9d0..d62c545 100644 (file)
@@ -10,7 +10,7 @@ class ExtendedEncoder(json.JSONEncoder):
 
     def default(self, obj):
         if isinstance(obj, Promise):
-            return unicode(obj)
+            return str(obj)
 
         if isinstance(obj, datetime):
             return datetime.ctime(obj) + " " + (datetime.tzname(obj) or 'GMT')
index c539908..fb20392 100644 (file)
@@ -18,7 +18,7 @@ class Theme(models.Model):
         verbose_name = _('theme')
         verbose_name_plural = _('themes')
 
-    def __unicode__(self):
+    def __str__(self):
         return self.name
 
     def __repr__(self):
index e1ef6ae..5dc770f 100644 (file)
@@ -3,7 +3,7 @@ from datetime import datetime
 import os
 import logging
 from time import mktime
-import urllib
+from urllib.parse import quote
 
 from django.conf import settings
 from django.core.urlresolvers import reverse
@@ -11,14 +11,13 @@ from django import http
 from django.http import Http404, HttpResponseForbidden
 from django.middleware.gzip import GZipMiddleware
 from django.utils.decorators import decorator_from_middleware
-from django.utils.encoding import smart_unicode
 from django.utils.formats import localize
 from django.utils.translation import ugettext as _
 from django.views.decorators.http import require_POST, require_GET
 from django.shortcuts import get_object_or_404, render
 
 from catalogue.models import Book, Chunk
-import nice_diff
+from . import nice_diff
 from wiki import forms
 from wiki.helpers import (JSONResponse, JSONFormInvalid, JSONServerError,
                 ajax_require_permission)
@@ -203,22 +202,22 @@ def revert(request, chunk_id):
 def gallery(request, directory):
     try:
         base_url = ''.join((
-                        smart_unicode(settings.MEDIA_URL),
-                        smart_unicode(settings.IMAGE_DIR),
-                        smart_unicode(directory)))
+                        settings.MEDIA_URL,
+                        settings.IMAGE_DIR,
+                        directory))
 
-        base_dir = os.path.join(
-                    smart_unicode(settings.MEDIA_ROOT),
-                    smart_unicode(settings.IMAGE_DIR),
-                    smart_unicode(directory))
+        base_dir = os.path.join((
+                    settings.MEDIA_ROOT,
+                    settings.IMAGE_DIR,
+                    directory))
 
         def map_to_url(filename):
-            return urllib.quote(("%s/%s" % (base_url, smart_unicode(filename))).encode('utf-8'))
+            return quote(("%s/%s" % (base_url, filename)))
 
         def is_image(filename):
             return os.path.splitext(filename)[1].lower() in (u'.jpg', u'.jpeg', u'.png')
 
-        images = [map_to_url(f) for f in map(smart_unicode, os.listdir(base_dir)) if is_image(f)]
+        images = [map_to_url(f) for f in os.listdir(base_dir) if is_image(f)]
         images.sort()
 
         books = Book.objects.filter(gallery=directory)
@@ -281,7 +280,7 @@ def history(request, chunk_id):
                 "author": change.author_str(),
                 "date": localize(change.created_at),
                 "publishable": _("Publishable") + "\n" if change.publishable else "",
-                "tag": ',\n'.join(unicode(tag) for tag in change.tags.all()),
+                "tag": ',\n'.join(str(tag) for tag in change.tags.all()),
                 "published": _("Published") + ": " + \
                     localize(change.publish_log.order_by('-book_record__timestamp')[0].book_record.timestamp) \
                     if change.publish_log.exists() else "",
index 2b8dd67..e5bbf41 100644 (file)
@@ -129,7 +129,7 @@ def history(request, object_id):
                 "author": change.author_str(),
                 "date": localize(change.created_at),
                 "publishable": _("Publishable") + "\n" if change.publishable else "",
-                "tag": ',\n'.join(unicode(tag) for tag in change.tags.all()),
+                "tag": ',\n'.join(str(tag) for tag in change.tags.all()),
             })
     return JSONResponse(changes)