From: Radek Czajka Date: Tue, 14 Aug 2012 11:58:49 +0000 (+0200) Subject: added XmlUpdater for massive XML updates X-Git-Url: https://git.mdrn.pl/redakcja.git/commitdiff_plain/0f7ebe49c20394d80afb129b29e4afa411347441 added XmlUpdater for massive XML updates also: fixdc command instead of fix_rdf_about, now fixes wluri-s. --- diff --git a/apps/catalogue/feeds.py b/apps/catalogue/feeds.py old mode 100755 new mode 100644 diff --git a/apps/catalogue/helpers.py b/apps/catalogue/helpers.py index bfc842fc..0009c6db 100644 --- a/apps/catalogue/helpers.py +++ b/apps/catalogue/helpers.py @@ -139,8 +139,6 @@ class GalleryMerger(object): renamed_files_other[f] = self.set_prefix(f, 1, True) # finally, move / rename files. - from nose.tools import set_trace - # set_trace() for frm, to in renamed_files.items(): move(join(self.path(self.dest), frm), join(self.path(self.dest), to)) diff --git a/apps/catalogue/management/__init__.py b/apps/catalogue/management/__init__.py old mode 100755 new mode 100644 index e69de29b..f7731d72 --- a/apps/catalogue/management/__init__.py +++ b/apps/catalogue/management/__init__.py @@ -0,0 +1,122 @@ +# -*- 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. +# +from collections import defaultdict +from django.db import transaction +from lxml import etree + + +class XmlUpdater(object): + """A base class for massive XML updates. + + In a subclass, override `fix_tree` and/or use `fixes_field` decorator. + Attributes: + * commit_desc: commits description + * retain_publishable: set publishable if head is (default: True) + * only_first_chunk: process only first chunks of books (default: False) + """ + commit_desc = "auto-update" + retain_publishable = True + only_first_chunk = False + + _element_fixers = defaultdict(list) + + def __init__(self): + self.counters = defaultdict(lambda: 0) + + @classmethod + def fixes_elements(cls, xpath): + """Decorator, registering a function as a fixer for given field type. + + Any decorated function will be called like + f(element, change=..., verbose=...) + providing changeset as context. + + :param xpath: element lookup, e.g. ".//{namespace-uri}tag-name" + :returns: True if anything changed + """ + def wrapper(fixer): + cls._element_fixers[xpath].append(fixer) + return fixer + return wrapper + + def fix_tree(self, tree, verbose): + """Override to provide general tree-fixing mechanism. + + :param tree: the parsed XML tree + :param verbose: verbosity level + :returns: True if anythig changed + """ + return False + + 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() + old_head = chunk.head + src = old_head.materialize() + try: + tree = etree.fromstring(src) + except: + if verbose: + print "%s: invalid XML" % chunk.get_absolute_url() + self.counters['Bad XML'] += 1 + return + + dirty = False + # Call the general fixing function. + if self.fix_tree(tree, verbose=verbose): + dirty = True + # Call the registered fixers. + for xpath, fixers in self._element_fixers.items(): + for elem in tree.findall(xpath): + for fixer in fixers: + if fixer(elem, change=old_head, verbose=verbose): + dirty = True + + if not dirty: + self.counters['Clean'] += 1 + return + + if not dry_run: + new_head = chunk.commit( + etree.tostring(tree, encoding=unicode), + author=user, + description=self.commit_desc + ) + if self.retain_publishable: + if old_head.publishable: + new_head.set_publishable(True) + if verbose >= 2: + print "done" + self.counters['Updated chunks'] += 1 + + def run(self, user, verbose=0, dry_run=False, books=None): + """Runs the actual update.""" + if books is None: + from catalogue.models import Book + books = Book.objects.all() + + # Start transaction management. + transaction.commit_unless_managed() + transaction.enter_transaction_management() + transaction.managed(True) + + for book in books: + self.counters['All books'] += 1 + chunks = book.chunk_set.all() + if self.only_first_chunk: + chunks = chunks[:1] + for chunk in chunks: + self.counters['All chunks'] += 1 + self.fix_chunk(chunk, user, verbose, dry_run) + + transaction.commit() + transaction.leave_transaction_management() + + def print_results(self): + """Prints the counters.""" + for item in sorted(self.counters.items()): + print "%s: %d" % item diff --git a/apps/catalogue/management/commands/__init__.py b/apps/catalogue/management/commands/__init__.py old mode 100755 new mode 100644 index e69de29b..e6f146f8 --- a/apps/catalogue/management/commands/__init__.py +++ b/apps/catalogue/management/commands/__init__.py @@ -0,0 +1,43 @@ +# -*- 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 optparse import make_option +from django.contrib.auth.models import User +from django.core.management.base import BaseCommand +from catalogue.models import Book + + +class XmlUpdaterCommand(BaseCommand): + """Base class for creating massive XML-updating commands. + + 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 handle(self, *args, **options): + verbose = options.get('verbose') + dry_run = options.get('dry_run') + username = options.get('username') + + if username: + user = User.objects.get(username=username) + else: + print 'Please provide a username.' + sys.exit(1) + + books = Book.objects.filter(slug__in=args) if args else None + + updater = self.updater() + updater.run(user, verbose=verbose, dry_run=dry_run, books=books) + updater.print_results() diff --git a/apps/catalogue/management/commands/assign_from_redmine.py b/apps/catalogue/management/commands/assign_from_redmine.py old mode 100755 new mode 100644 diff --git a/apps/catalogue/management/commands/fix_rdf_about.py b/apps/catalogue/management/commands/fix_rdf_about.py deleted file mode 100755 index c252c208..00000000 --- a/apps/catalogue/management/commands/fix_rdf_about.py +++ /dev/null @@ -1,89 +0,0 @@ -# -*- coding: utf-8 -*- - -from optparse import make_option - -from django.contrib.auth.models import User -from django.core.management.base import BaseCommand -from django.db import transaction - -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='Less output'), - make_option('-d', '--dry-run', action='store_true', dest='dry_run', - default=False, help="Don't actually touch anything"), - ) - help = 'Updates the rdf:about metadata field.' - - def handle(self, *args, **options): - from lxml import etree - - verbose = options.get('verbose') - dry_run = options.get('dry_run') - - # Start transaction management. - transaction.commit_unless_managed() - transaction.enter_transaction_management() - transaction.managed(True) - - all_books = 0 - nonxml = 0 - nordf = 0 - already = 0 - done = 0 - - for b in Book.objects.all(): - all_books += 1 - if verbose: - print "%s: " % b.title, - chunk = b[0] - old_head = chunk.head - src = old_head.materialize() - - try: - t = etree.fromstring(src) - except: - nonxml += 1 - if verbose: - print "invalid XML" - continue - desc = t.find(".//{http://www.w3.org/1999/02/22-rdf-syntax-ns#}Description") - if desc is None: - nordf += 1 - if verbose: - print "no RDF found" - continue - - correct_about = b.correct_about() - attr_name = "{http://www.w3.org/1999/02/22-rdf-syntax-ns#}about" - if desc.get(attr_name) == correct_about: - already += 1 - if verbose: - print "already correct" - continue - desc.set(attr_name, correct_about) - if not dry_run: - new_head = chunk.commit(etree.tostring(t, encoding=unicode), - author_name='platforma redakcyjna', - description='auto-update rdf:about' - ) - # retain the publishable status - if old_head.publishable: - new_head.set_publishable(True) - if verbose: - print "done" - done += 1 - - # Print results - print "All books: ", all_books - print "Invalid XML: ", nonxml - print "No RDF found: ", nordf - print "Already correct: ", already - print "Books updated: ", done - - transaction.commit() - transaction.leave_transaction_management() - diff --git a/apps/catalogue/management/commands/fixdc.py b/apps/catalogue/management/commands/fixdc.py new file mode 100644 index 00000000..80b341c7 --- /dev/null +++ b/apps/catalogue/management/commands/fixdc.py @@ -0,0 +1,48 @@ +# -*- 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. +# +from librarian import RDFNS, WLURI, ValidationError +from librarian.dcparser import BookInfo +from catalogue.management import XmlUpdater +from catalogue.management.commands import XmlUpdaterCommand + + +class FixDC(XmlUpdater): + commit_desc = "auto-fixing DC" + retain_publishable = True + only_first_chunk = True + + def fix_wluri(elem, change, verbose): + try: + WLURI.strict(elem.text) + except ValidationError: + correct_field = unicode(WLURI.from_slug(WLURI(elem.text).slug)) + if verbose: + print "Changing %s from %s to %s" % ( + elem.tag, elem.text, correct_field + ) + elem.text = correct_field + return True + for field in BookInfo.FIELDS: + if field.validator == WLURI: + XmlUpdater.fixes_elements('.//' + field.uri)(fix_wluri) + + @XmlUpdater.fixes_elements(".//" + RDFNS("Description")) + def fix_rdfabout(elem, change, verbose): + correct_about = change.tree.book.correct_about() + attr_name = RDFNS("about") + current_about = elem.get(attr_name) + if current_about != correct_about: + if verbose: + print "Changing rdf:about from %s to %s" % ( + current_about, correct_about + ) + elem.set(attr_name, correct_about) + return True + + +class Command(XmlUpdaterCommand): + updater = FixDC + help = 'Fixes obvious errors in DC: rdf:about and WLURI format.' diff --git a/apps/catalogue/management/commands/import_wl.py b/apps/catalogue/management/commands/import_wl.py old mode 100755 new mode 100644 diff --git a/apps/catalogue/management/commands/merge_books.py b/apps/catalogue/management/commands/merge_books.py old mode 100755 new mode 100644 diff --git a/apps/catalogue/test_utils.py b/apps/catalogue/test_utils.py new file mode 100644 index 00000000..2b085450 --- /dev/null +++ b/apps/catalogue/test_utils.py @@ -0,0 +1,14 @@ +# -*- 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. +# +"""Testing utilities.""" + +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') diff --git a/apps/catalogue/tests/__init__.py b/apps/catalogue/tests/__init__.py old mode 100755 new mode 100644 index ae7bef7d..533a6c53 --- a/apps/catalogue/tests/__init__.py +++ b/apps/catalogue/tests/__init__.py @@ -1,224 +1,9 @@ -from os.path import abspath, dirname, join, basename, exists -from os import makedirs, listdir -from nose.tools import * -from mock import patch -from django.test import TestCase -from django.contrib.auth.models import User -from catalogue.models import Book, BookPublishRecord -from tempfile import mkdtemp -from django.conf import settings - -def get_fixture(path): - f_path = join(dirname(abspath(__file__)), 'files', path) - with open(f_path) as f: - return unicode(f.read(), 'utf-8') - - -class PublishTests(TestCase): - - def setUp(self): - self.user = User.objects.create(username='tester') - self.text1 = get_fixture('chunk1.xml') - self.book = Book.create(self.user, self.text1, slug='test-book') - - @patch('apiclient.api_call') - def test_unpublishable(self, api_call): - with self.assertRaises(AssertionError): - self.book.publish(self.user) - - @patch('apiclient.api_call') - def test_publish(self, api_call): - self.book[0].head.set_publishable(True) - self.book.publish(self.user) - api_call.assert_called_with(self.user, 'books/', {"book_xml": self.text1}) - - @patch('apiclient.api_call') - def test_publish_multiple(self, api_call): - self.book[0].head.set_publishable(True) - self.book[0].split(slug='part-2') - self.book[1].commit(get_fixture('chunk2.xml')) - self.book[1].head.set_publishable(True) - self.book.publish(self.user) - api_call.assert_called_with(self.user, 'books/', {"book_xml": get_fixture('expected.xml')}) - - -class ManipulationTests(TestCase): - - def setUp(self): - self.user = User.objects.create(username='tester') - self.book1 = Book.create(self.user, 'book 1', slug='book1') - self.book2 = Book.create(self.user, 'book 2', slug='book2') - - def test_append(self): - self.book1.append(self.book2) - self.assertEqual(Book.objects.all().count(), 1) - self.assertEqual(len(self.book1), 2) - - def test_append_to_self(self): - with self.assertRaises(AssertionError): - self.book1.append(Book.objects.get(pk=self.book1.pk)) - self.assertEqual(Book.objects.all().count(), 2) - self.assertEqual(len(self.book1), 1) - - def test_prepend_history(self): - self.book1.prepend_history(self.book2) - self.assertEqual(Book.objects.all().count(), 1) - self.assertEqual(len(self.book1), 1) - self.assertEqual(self.book1.materialize(), 'book 1') - - def test_prepend_history_to_self(self): - with self.assertRaises(AssertionError): - self.book1.prepend_history(self.book1) - self.assertEqual(Book.objects.all().count(), 2) - self.assertEqual(self.book1.materialize(), 'book 1') - self.assertEqual(self.book2.materialize(), 'book 2') - - def test_split_book(self): - self.book1.chunk_set.create(number=2, title='Second chunk', - slug='book3') - self.book1[1].commit('I survived!') - self.assertEqual(len(self.book1), 2) - self.book1.split() - self.assertEqual(set([b.slug for b in Book.objects.all()]), - set(['book2', '1', 'book3'])) - self.assertEqual( - Book.objects.get(slug='book3').materialize(), - 'I survived!') - - -class GalleryAppendTests(TestCase): - def setUp(self): - self.user = User.objects.create(username='tester') - self.book1 = Book.create(self.user, 'book 1', slug='book1') - self.book1.chunk_set.create(number=2, title='Second chunk', - slug='book 1 / 2') - c=self.book1[0] - c.gallery_start=1 - c=self.book1[1] - c.gallery_start=3 - - self.scandir = join(settings.MEDIA_ROOT, settings.IMAGE_DIR) - if not exists(self.scandir): - makedirs(self.scandir) - - def make_gallery(self, book, files): - d = mkdtemp('gallery', dir=self.scandir) - for named, cont in files.items(): - f = open(join(d, named), 'w') - f.write(cont) - f.close() - book.gallery = basename(d) - - - def test_both_indexed(self): - self.book2 = Book.create(self.user, 'book 2', slug='book2') - self.book2.chunk_set.create(number=2, title='Second chunk of second book', - slug='book 2 / 2') - - c = self.book2[0] - c.gallery_start = 1 - c.save() - c = self.book2[1] - c.gallery_start = 3 - c.save() - - print "gallery starts:",self.book2[0].gallery_start, self.book2[1].gallery_start - - self.make_gallery(self.book1, { - '1-0001_1l' : 'aa', - '1-0001_2r' : 'bb', - '1-0002_1l' : 'cc', - '1-0002_2r' : 'dd', - }) - - self.make_gallery(self.book2, { - '1-0001_1l' : 'dd', # the same, should not be moved - '1-0001_2r' : 'ff', - '2-0002_1l' : 'gg', - '2-0002_2r' : 'hh', - }) - - self.book1.append(self.book2) - - files = listdir(join(self.scandir, self.book1.gallery)) - files.sort() - print files - self.assertEqual(files, [ - '1-0001_1l', - '1-0001_2r', - '1-0002_1l', - '1-0002_2r', - # '2-0001_1l', - '2-0001_2r', - '3-0002_1l', - '3-0002_2r', - ]) - - self.assertEqual((4, 6), (self.book1[2].gallery_start, self.book1[3].gallery_start)) - - - def test_none_indexed(self): - self.book2 = Book.create(self.user, 'book 2', slug='book2') - self.make_gallery(self.book1, { - '0001_1l' : 'aa', - '0001_2r' : 'bb', - '0002_1l' : 'cc', - '0002_2r' : 'dd', - }) - - self.make_gallery(self.book2, { - '0001_1l' : 'ee', - '0001_2r' : 'ff', - '0002_1l' : 'gg', - '0002_2r' : 'hh', - }) - - self.book1.append(self.book2) - - files = listdir(join(self.scandir, self.book1.gallery)) - files.sort() - print files - self.assertEqual(files, [ - '0-0001_1l', - '0-0001_2r', - '0-0002_1l', - '0-0002_2r', - '1-0001_1l', - '1-0001_2r', - '1-0002_1l', - '1-0002_2r', - ]) - - - def test_none_indexed(self): - import nose.tools - self.book2 = Book.create(self.user, 'book 2', slug='book2') - self.make_gallery(self.book1, { - '1-0001_1l' : 'aa', - '1-0001_2r' : 'bb', - '1002_1l' : 'cc', - '1002_2r' : 'dd', - }) - - self.make_gallery(self.book2, { - '0001_1l' : 'ee', - '0001_2r' : 'ff', - '0002_1l' : 'gg', - '0002_2r' : 'hh', - }) - - self.book1.append(self.book2) - - files = listdir(join(self.scandir, self.book1.gallery)) - files.sort() - print files - self.assertEqual(files, [ - '0-1-0001_1l', - '0-1-0001_2r', - '0-1002_1l', - '0-1002_2r', - '1-0001_1l', - '1-0001_2r', - '1-0002_1l', - '1-0002_2r', - ]) +# -*- 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. +# +from catalogue.tests.book import * +from catalogue.tests.gallery import * +from catalogue.tests.publish import * +from catalogue.tests.xml_updater import * diff --git a/apps/catalogue/tests/book.py b/apps/catalogue/tests/book.py new file mode 100644 index 00000000..df6f3b4f --- /dev/null +++ b/apps/catalogue/tests/book.py @@ -0,0 +1,54 @@ +# -*- 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. +# +"""Tests for manipulating books in the catalogue.""" + +from django.test import TestCase +from django.contrib.auth.models import User +from catalogue.models import Book + + +class ManipulationTests(TestCase): + + def setUp(self): + self.user = User.objects.create(username='tester') + self.book1 = Book.create(self.user, 'book 1', slug='book1') + self.book2 = Book.create(self.user, 'book 2', slug='book2') + + def test_append(self): + self.book1.append(self.book2) + self.assertEqual(Book.objects.all().count(), 1) + self.assertEqual(len(self.book1), 2) + + def test_append_to_self(self): + with self.assertRaises(AssertionError): + self.book1.append(Book.objects.get(pk=self.book1.pk)) + self.assertEqual(Book.objects.all().count(), 2) + self.assertEqual(len(self.book1), 1) + + def test_prepend_history(self): + self.book1.prepend_history(self.book2) + self.assertEqual(Book.objects.all().count(), 1) + self.assertEqual(len(self.book1), 1) + self.assertEqual(self.book1.materialize(), 'book 1') + + def test_prepend_history_to_self(self): + with self.assertRaises(AssertionError): + self.book1.prepend_history(self.book1) + self.assertEqual(Book.objects.all().count(), 2) + self.assertEqual(self.book1.materialize(), 'book 1') + self.assertEqual(self.book2.materialize(), 'book 2') + + def test_split_book(self): + self.book1.chunk_set.create(number=2, title='Second chunk', + slug='book3') + self.book1[1].commit('I survived!') + self.assertEqual(len(self.book1), 2) + self.book1.split() + self.assertEqual(set([b.slug for b in Book.objects.all()]), + set(['book2', '1', 'book3'])) + self.assertEqual( + Book.objects.get(slug='book3').materialize(), + 'I survived!') diff --git a/apps/catalogue/tests/gallery.py b/apps/catalogue/tests/gallery.py new file mode 100644 index 00000000..e1f636dc --- /dev/null +++ b/apps/catalogue/tests/gallery.py @@ -0,0 +1,153 @@ +# -*- 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. +# +"""Tests for galleries of scans.""" + +from os.path import join, basename, exists +from os import makedirs, listdir +from django.test import TestCase +from django.contrib.auth.models import User +from catalogue.models import Book +from tempfile import mkdtemp +from django.conf import settings + + +class GalleryAppendTests(TestCase): + def setUp(self): + self.user = User.objects.create(username='tester') + self.book1 = Book.create(self.user, 'book 1', slug='book1') + self.book1.chunk_set.create(number=2, title='Second chunk', + slug='book 1 / 2') + c=self.book1[0] + c.gallery_start=1 + c=self.book1[1] + c.gallery_start=3 + + self.scandir = join(settings.MEDIA_ROOT, settings.IMAGE_DIR) + if not exists(self.scandir): + makedirs(self.scandir) + + def make_gallery(self, book, files): + d = mkdtemp('gallery', dir=self.scandir) + for named, cont in files.items(): + f = open(join(d, named), 'w') + f.write(cont) + f.close() + book.gallery = basename(d) + + + def test_both_indexed(self): + self.book2 = Book.create(self.user, 'book 2', slug='book2') + self.book2.chunk_set.create(number=2, title='Second chunk of second book', + slug='book 2 / 2') + + c = self.book2[0] + c.gallery_start = 1 + c.save() + c = self.book2[1] + c.gallery_start = 3 + c.save() + + print "gallery starts:",self.book2[0].gallery_start, self.book2[1].gallery_start + + self.make_gallery(self.book1, { + '1-0001_1l' : 'aa', + '1-0001_2r' : 'bb', + '1-0002_1l' : 'cc', + '1-0002_2r' : 'dd', + }) + + self.make_gallery(self.book2, { + '1-0001_1l' : 'dd', # the same, should not be moved + '1-0001_2r' : 'ff', + '2-0002_1l' : 'gg', + '2-0002_2r' : 'hh', + }) + + self.book1.append(self.book2) + + files = listdir(join(self.scandir, self.book1.gallery)) + files.sort() + print files + self.assertEqual(files, [ + '1-0001_1l', + '1-0001_2r', + '1-0002_1l', + '1-0002_2r', + # '2-0001_1l', + '2-0001_2r', + '3-0002_1l', + '3-0002_2r', + ]) + + self.assertEqual((4, 6), (self.book1[2].gallery_start, self.book1[3].gallery_start)) + + + def test_none_indexed(self): + self.book2 = Book.create(self.user, 'book 2', slug='book2') + self.make_gallery(self.book1, { + '0001_1l' : 'aa', + '0001_2r' : 'bb', + '0002_1l' : 'cc', + '0002_2r' : 'dd', + }) + + self.make_gallery(self.book2, { + '0001_1l' : 'ee', + '0001_2r' : 'ff', + '0002_1l' : 'gg', + '0002_2r' : 'hh', + }) + + self.book1.append(self.book2) + + files = listdir(join(self.scandir, self.book1.gallery)) + files.sort() + print files + self.assertEqual(files, [ + '0-0001_1l', + '0-0001_2r', + '0-0002_1l', + '0-0002_2r', + '1-0001_1l', + '1-0001_2r', + '1-0002_1l', + '1-0002_2r', + ]) + + + def test_none_indexed(self): + import nose.tools + self.book2 = Book.create(self.user, 'book 2', slug='book2') + self.make_gallery(self.book1, { + '1-0001_1l' : 'aa', + '1-0001_2r' : 'bb', + '1002_1l' : 'cc', + '1002_2r' : 'dd', + }) + + self.make_gallery(self.book2, { + '0001_1l' : 'ee', + '0001_2r' : 'ff', + '0002_1l' : 'gg', + '0002_2r' : 'hh', + }) + + self.book1.append(self.book2) + + files = listdir(join(self.scandir, self.book1.gallery)) + files.sort() + print files + self.assertEqual(files, [ + '0-1-0001_1l', + '0-1-0001_2r', + '0-1002_1l', + '0-1002_2r', + '1-0001_1l', + '1-0001_2r', + '1-0002_1l', + '1-0002_2r', + ]) + diff --git a/apps/catalogue/tests/publish.py b/apps/catalogue/tests/publish.py new file mode 100644 index 00000000..9f0b8ca4 --- /dev/null +++ b/apps/catalogue/tests/publish.py @@ -0,0 +1,40 @@ +# -*- 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. +# +"""Tests for the publishing process.""" + +from catalogue.test_utils import get_fixture + +from mock import patch +from django.test import TestCase +from django.contrib.auth.models import User +from catalogue.models import Book + + +class PublishTests(TestCase): + def setUp(self): + self.user = User.objects.create(username='tester') + self.text1 = get_fixture('chunk1.xml') + self.book = Book.create(self.user, self.text1, slug='test-book') + + @patch('apiclient.api_call') + def test_unpublishable(self, api_call): + with self.assertRaises(AssertionError): + self.book.publish(self.user) + + @patch('apiclient.api_call') + def test_publish(self, api_call): + self.book[0].head.set_publishable(True) + self.book.publish(self.user) + api_call.assert_called_with(self.user, 'books/', {"book_xml": self.text1}) + + @patch('apiclient.api_call') + def test_publish_multiple(self, api_call): + self.book[0].head.set_publishable(True) + self.book[0].split(slug='part-2') + self.book[1].commit(get_fixture('chunk2.xml')) + self.book[1].head.set_publishable(True) + self.book.publish(self.user) + api_call.assert_called_with(self.user, 'books/', {"book_xml": get_fixture('expected.xml')}) diff --git a/apps/catalogue/tests/xml_updater.py b/apps/catalogue/tests/xml_updater.py new file mode 100644 index 00000000..9fb5a4a0 --- /dev/null +++ b/apps/catalogue/tests/xml_updater.py @@ -0,0 +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. +# +"""XmlUpdater tests.""" + +from catalogue.test_utils import get_fixture +from django.test import TestCase +from django.contrib.auth.models import User +from catalogue.models import Book +from catalogue.management import XmlUpdater +from librarian import DCNS + + +class XmlUpdaterTests(TestCase): + class SimpleUpdater(XmlUpdater): + @XmlUpdater.fixes_elements('.//' + DCNS('title')) + def fix_title(element, **kwargs): + element.text = element.text + " fixed" + return True + + def setUp(self): + self.user = User.objects.create(username='tester') + text = get_fixture('chunk1.xml') + Book.create(self.user, text, slug='test-book') + self.title = "Do M***" + + def test_xml_updater(self): + self.SimpleUpdater().run(self.user) + self.assertEqual( + Book.objects.get(slug='test-book').wldocument( + publishable=False).book_info.title, + self.title + " fixed" + )