slightly improve book.publish
[redakcja.git] / apps / catalogue / management / commands / merge_books.py
1 # -*- coding: utf-8 -*-
2
3 from optparse import make_option
4 import sys
5
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
10
11 from catalogue.models import Book
12
13
14 def common_prefix(texts):
15     common = []
16
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]))
20         if len(chars) > 1:
21             break
22         common.append(chars[0])
23     return "".join(common)
24
25
26 class Command(BaseCommand):
27     option_list = BaseCommand.option_list + (
28         make_option('-s', '--slug', dest='new_slug', metavar='SLUG',
29             help='New slug of the merged book (defaults to common part of all slugs).'),
30         make_option('-t', '--title', dest='new_title', metavar='TITLE',
31             help='New title of the merged book (defaults to common part of all titles).'),
32         make_option('-q', '--quiet', action='store_false', dest='verbose', default=True,
33             help='Less output'),
34         make_option('-g', '--guess', action='store_true', dest='guess', default=False,
35             help='Try to guess what merges are needed (but do not apply them).'),
36         make_option('-d', '--dry-run', action='store_true', dest='dry_run', default=False,
37             help='Dry run: do not actually change anything.'),
38         make_option('-f', '--force', action='store_true', dest='force', default=False,
39             help='On slug conflict, hide the original book to archive.'),
40     )
41     help = 'Merges multiple books into one.'
42     args = '[slug]...'
43
44
45     def print_guess(self, dry_run=True, force=False):
46         from collections import defaultdict
47         from pipes import quote
48         import re
49     
50         def read_slug(slug):
51             res = []
52             res.append((re.compile(ur'__?(przedmowa)$'), -1))
53             res.append((re.compile(ur'__?(cz(esc)?|ksiega|rozdzial)__?(?P<n>\d*)$'), None))
54             res.append((re.compile(ur'__?(rozdzialy__?)?(?P<n>\d*)-'), None))
55         
56             for r, default in res:
57                 m = r.search(slug)
58                 if m:
59                     start = m.start()
60                     try:
61                         return int(m.group('n')), slug[:start]
62                     except IndexError:
63                         return default, slug[:start]
64             return None, slug
65     
66         def file_to_title(fname):
67             """ Returns a title-like version of a filename. """
68             parts = (p.replace('_', ' ').title() for p in fname.split('__'))
69             return ' / '.join(parts)
70     
71         merges = defaultdict(list)
72         slugs = []
73         for b in Book.objects.all():
74             slugs.append(b.slug)
75             n, ns = read_slug(b.slug)
76             if n is not None:
77                 merges[ns].append((n, b))
78     
79         conflicting_slugs = []
80         for slug in sorted(merges.keys()):
81             merge_list = sorted(merges[slug])
82             if len(merge_list) < 2:
83                 continue
84     
85             merge_slugs = [b.slug for i, b in merge_list]
86             if slug in slugs and slug not in merge_slugs:
87                 conflicting_slugs.append(slug)
88     
89             title = file_to_title(slug)
90             print "./manage.py merge_books %s%s--title=%s --slug=%s \\\n    %s\n" % (
91                 '--dry-run ' if dry_run else '',
92                 '--force ' if force else '',
93                 quote(title), slug,
94                 " \\\n    ".join(merge_slugs)
95                 )
96     
97         if conflicting_slugs:
98             if force:
99                 print self.style.NOTICE('# These books will be archived:')
100             else:
101                 print self.style.ERROR('# ERROR: Conflicting slugs:')
102             for slug in conflicting_slugs:
103                 print '#', slug
104
105
106     def handle(self, *slugs, **options):
107
108         self.style = color_style()
109
110         force = options.get('force')
111         guess = options.get('guess')
112         dry_run = options.get('dry_run')
113         new_slug = options.get('new_slug').decode('utf-8')
114         new_title = options.get('new_title').decode('utf-8')
115         verbose = options.get('verbose')
116
117         if guess:
118             if slugs:
119                 print "Please specify either slugs, or --guess."
120                 return
121             else:
122                 self.print_guess(dry_run, force)
123                 return
124         if not slugs:
125             print "Please specify some book slugs"
126             return
127
128         # Start transaction management.
129         transaction.enter_transaction_management()
130
131         books = [Book.objects.get(slug=slug) for slug in slugs]
132         common_slug = common_prefix(slugs)
133         common_title = common_prefix([b.title for b in books])
134
135         if not new_title:
136             new_title = common_title
137         elif common_title.startswith(new_title):
138             common_title = new_title
139
140         if not new_slug:
141             new_slug = common_slug
142         elif common_slug.startswith(new_slug):
143             common_slug = new_slug
144
145         if slugs[0] != new_slug and Book.objects.filter(slug=new_slug).exists():
146             self.style.ERROR('Book already exists, skipping!')
147
148
149         if dry_run and verbose:
150             print self.style.NOTICE('DRY RUN: nothing will be changed.')
151             print
152
153         if verbose:
154             print "New title:", self.style.NOTICE(new_title)
155             print "New slug:", self.style.NOTICE(new_slug)
156             print
157
158         for i, book in enumerate(books):
159             chunk_titles = []
160             chunk_slugs = []
161
162             book_title = book.title[len(common_title):].replace(' / ', ' ').lstrip()
163             book_slug = book.slug[len(common_slug):].replace('__', '_').lstrip('-_')
164             for j, chunk in enumerate(book):
165                 if j:
166                     new_chunk_title = book_title + '_%d' % j
167                     new_chunk_slug = book_slug + '_%d' % j
168                 else:
169                     new_chunk_title, new_chunk_slug = book_title, book_slug
170
171                 chunk_titles.append(new_chunk_title)
172                 chunk_slugs.append(new_chunk_slug)
173
174                 if verbose:
175                     print "title: %s // %s  -->\n       %s // %s\nslug: %s / %s  -->\n      %s / %s" % (
176                         book.title, chunk.title,
177                         new_title, new_chunk_title,
178                         book.slug, chunk.slug,
179                         new_slug, new_chunk_slug)
180                     print
181
182             if not dry_run:
183                 try:
184                     conflict = Book.objects.get(slug=new_slug)
185                 except Book.DoesNotExist:
186                     conflict = None
187                 else:
188                     if conflict == books[0]:
189                         conflict = None
190
191                 if conflict:
192                     if force:
193                         # FIXME: there still may be a conflict
194                         conflict.slug = '.' + conflict.slug
195                         conflict.save()
196                         print self.style.NOTICE('Book with slug "%s" moved to "%s".' % (new_slug, conflict.slug))
197                     else:
198                         print self.style.ERROR('ERROR: Book with slug "%s" exists.' % new_slug)
199                         return
200
201                 if i:
202                     books[0].append(books[i], slugs=chunk_slugs, titles=chunk_titles)
203                 else:
204                     book.title = new_title
205                     book.slug = new_slug
206                     book.save()
207                     for j, chunk in enumerate(book):
208                         chunk.title = chunk_titles[j]
209                         chunk.slug = chunk_slugs[j]
210                         chunk.save()
211
212
213         transaction.commit()
214         transaction.leave_transaction_management()
215