1 # This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
2 # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
6 from django.contrib.auth.models import User
7 from django.core.management.base import BaseCommand
8 from django.core.management.color import color_style
9 from django.db import transaction
11 from catalogue.models import Book
14 def common_prefix(texts):
17 min_len = min(len(text) for text in texts)
18 for i in range(min_len):
19 chars = list(set([text[i] for text in texts]))
22 common.append(chars[0])
23 return "".join(common)
26 class Command(BaseCommand):
27 help = 'Merges multiple books into one.'
30 def add_arguments(self, parser):
32 '-s', '--slug', dest='new_slug', metavar='SLUG',
33 help='New slug of the merged book (defaults to common part of all slugs).')
35 '-t', '--title', dest='new_title', metavar='TITLE',
36 help='New title of the merged book (defaults to common part of all titles).')
38 '-q', '--quiet', action='store_false', dest='verbose', default=True,
41 '-g', '--guess', action='store_true', dest='guess', default=False,
42 help='Try to guess what merges are needed (but do not apply them).')
44 '-d', '--dry-run', action='store_true', dest='dry_run', default=False,
45 help='Dry run: do not actually change anything.')
47 '-f', '--force', action='store_true', dest='force', default=False,
48 help='On slug conflict, hide the original book to archive.')
50 def print_guess(self, dry_run=True, force=False):
51 from collections import defaultdict
52 from pipes import quote
57 res.append((re.compile(r'__?(przedmowa)$'), -1))
58 res.append((re.compile(r'__?(cz(esc)?|ksiega|rozdzial)__?(?P<n>\d*)$'), None))
59 res.append((re.compile(r'__?(rozdzialy__?)?(?P<n>\d*)-'), None))
61 for r, default in res:
66 return int(m.group('n')), slug[:start]
68 return default, slug[:start]
71 def file_to_title(fname):
72 """ Returns a title-like version of a filename. """
73 parts = (p.replace('_', ' ').title() for p in fname.split('__'))
74 return ' / '.join(parts)
76 merges = defaultdict(list)
78 for b in Book.objects.all():
80 n, ns = read_slug(b.slug)
82 merges[ns].append((n, b))
84 conflicting_slugs = []
85 for slug in sorted(merges.keys()):
86 merge_list = sorted(merges[slug])
87 if len(merge_list) < 2:
90 merge_slugs = [b.slug for i, b in merge_list]
91 if slug in slugs and slug not in merge_slugs:
92 conflicting_slugs.append(slug)
94 title = file_to_title(slug)
95 print("./manage.py merge_books %s%s--title=%s --slug=%s \\\n %s\n" % (
96 '--dry-run ' if dry_run else '',
97 '--force ' if force else '',
99 " \\\n ".join(merge_slugs)
102 if conflicting_slugs:
104 print(self.style.NOTICE('# These books will be archived:'))
106 print(self.style.ERROR('# ERROR: Conflicting slugs:'))
107 for slug in conflicting_slugs:
111 def handle(self, *slugs, **options):
113 self.style = color_style()
115 force = options.get('force')
116 guess = options.get('guess')
117 dry_run = options.get('dry_run')
118 new_slug = options.get('new_slug').decode('utf-8')
119 new_title = options.get('new_title').decode('utf-8')
120 verbose = options.get('verbose')
124 print("Please specify either slugs, or --guess.")
127 self.print_guess(dry_run, force)
130 print("Please specify some book slugs")
133 # Start transaction management.
134 transaction.enter_transaction_management()
136 books = [Book.objects.get(slug=slug) for slug in slugs]
137 common_slug = common_prefix(slugs)
138 common_title = common_prefix([b.title for b in books])
141 new_title = common_title
142 elif common_title.startswith(new_title):
143 common_title = new_title
146 new_slug = common_slug
147 elif common_slug.startswith(new_slug):
148 common_slug = new_slug
150 if slugs[0] != new_slug and Book.objects.filter(slug=new_slug).exists():
151 self.style.ERROR('Book already exists, skipping!')
154 if dry_run and verbose:
155 print(self.style.NOTICE('DRY RUN: nothing will be changed.'))
159 print("New title:", self.style.NOTICE(new_title))
160 print("New slug:", self.style.NOTICE(new_slug))
163 for i, book in enumerate(books):
167 book_title = book.title[len(common_title):].replace(' / ', ' ').lstrip()
168 book_slug = book.slug[len(common_slug):].replace('__', '_').lstrip('-_')
169 for j, chunk in enumerate(book):
171 new_chunk_title = book_title + '_%d' % j
172 new_chunk_slug = book_slug + '_%d' % j
174 new_chunk_title, new_chunk_slug = book_title, book_slug
176 chunk_titles.append(new_chunk_title)
177 chunk_slugs.append(new_chunk_slug)
180 print("title: %s // %s -->\n %s // %s\nslug: %s / %s -->\n %s / %s" % (
181 book.title, chunk.title,
182 new_title, new_chunk_title,
183 book.slug, chunk.slug,
184 new_slug, new_chunk_slug))
189 conflict = Book.objects.get(slug=new_slug)
190 except Book.DoesNotExist:
193 if conflict == books[0]:
198 # FIXME: there still may be a conflict
199 conflict.slug = '.' + conflict.slug
201 print(self.style.NOTICE('Book with slug "%s" moved to "%s".' % (new_slug, conflict.slug)))
203 print(self.style.ERROR('ERROR: Book with slug "%s" exists.' % new_slug))
207 books[0].append(books[i], slugs=chunk_slugs, titles=chunk_titles)
209 book.title = new_title
212 for j, chunk in enumerate(book):
213 chunk.title = chunk_titles[j]
214 chunk.slug = chunk_slugs[j]
219 transaction.leave_transaction_management()