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