예제 #1
0
    def test(self):
        tree = {
            'id': '[email protected]',
            'shortId': None,
            'contents': [
                {'id': '209deb1f-1a46-4369-9e0d-18674cf58a3e@7',
                 'shortId': None,
                 'title': '(title override)'},
                {'id': '[email protected]',
                 'shortId': '[email protected]',
                 'title': None,
                 'contents': [
                     {'id': 'f3c9ab70-a916-4d8c-9256-42953287b4e9@3',
                      'shortId': '88mrcKkW@3',
                      'title': '(another title override)'},
                     {'id': 'd395b566-5fe3-4428-bcb2-19016e3aa3ce@4',
                      'shortId': '05W1Zl_j@4',
                      'title': 'Physics: An Introduction'}]}]}
        nodes = self.target(tree)

        self.assertEqual(
            cnxepub.model_to_tree(nodes[0]),
            {'id': '209deb1f-1a46-4369-9e0d-18674cf58a3e@7',
             'shortId': 'IJ3rHxpG@7',
             'title': '(title override)'}
            )
        self.assertEqual(
            cnxepub.model_to_tree(nodes[1]),
            tree['contents'][1]
            )
예제 #2
0
    def test(self):
        tree = {
            'id': '[email protected]',
            'shortId': None,
            'contents': [
                {'id': '209deb1f-1a46-4369-9e0d-18674cf58a3e@7',
                 'shortId': None,
                 'title': '(title override)'},
                {'id': '[email protected]',
                 'shortId': '[email protected]',
                 'title': None,
                 'contents': [
                     {'id': 'f3c9ab70-a916-4d8c-9256-42953287b4e9@3',
                      'shortId': '88mrcKkW@3',
                      'title': '(another title override)'},
                     {'id': 'd395b566-5fe3-4428-bcb2-19016e3aa3ce@4',
                      'shortId': '05W1Zl_j@4',
                      'title': 'Physics: An Introduction'}]}]}
        nodes = self.target(tree)

        self.assertEqual(
            cnxepub.model_to_tree(nodes[0]),
            {'id': '209deb1f-1a46-4369-9e0d-18674cf58a3e@7',
             'shortId': 'IJ3rHxpG@7',
             'title': '(title override)'}
            )
        self.assertEqual(
            cnxepub.model_to_tree(nodes[1]),
            tree['contents'][1]
            )
예제 #3
0
파일: main.py 프로젝트: Dimmus/cnx-epub
def single_html(epub_file_path, html_out=sys.stdout, mathjax_version=None,
                numchapters=None, includes=None):
    """Generate complete book HTML."""
    epub = cnxepub.EPUB.from_file(epub_file_path)
    if len(epub) != 1:
        raise Exception('Expecting an epub with one book')

    package = epub[0]
    binder = cnxepub.adapt_package(package)
    partcount.update({}.fromkeys(parts, 0))
    partcount['book'] += 1

    html = cnxepub.SingleHTMLFormatter(binder, includes=includes)

    # Truncate binder to the first N chapters where N = numchapters.
    logger.debug('Full binder: {}'.format(cnxepub.model_to_tree(binder)))
    if numchapters is not None:
        apply_numchapters(html.get_node_type, binder, numchapters)
        logger.debug('Truncated Binder: {}'.format(
            cnxepub.model_to_tree(binder)))

    # Add mathjax to the page.
    if mathjax_version:
        etree.SubElement(
            html.head,
            'script',
            src=MATHJAX_URL.format(mathjax_version=mathjax_version))

    print(str(html), file=html_out)
    if hasattr(html_out, 'name'):
        # html_out is a file, close after writing
        html_out.close()
예제 #4
0
def collate(binder, publisher, message, cursor, includes=None):
    """Given a `Binder` as `binder`, collate the contents and
    persist those changes alongside the published content.

    """

    binder = collate_models(binder, ruleset="ruleset.css", includes=includes)

    def flatten_filter(model):
        return isinstance(model, cnxepub.CompositeDocument)

    def only_documents_filter(model):
        return isinstance(model, cnxepub.Document) \
               and not isinstance(model, cnxepub.CompositeDocument)

    for doc in cnxepub.flatten_to(binder, flatten_filter):
        publish_composite_model(cursor, doc, binder, publisher, message)

    for doc in cnxepub.flatten_to(binder, only_documents_filter):
        publish_collated_document(cursor, doc, binder)

    tree = cnxepub.model_to_tree(binder)
    publish_collated_tree(cursor, tree)

    return []
예제 #5
0
def collate(binder, publisher, message, cursor, includes=None):
    """Given a `Binder` as `binder`, collate the contents and
    persist those changes alongside the published content.

    """

    binder = collate_models(binder, ruleset="ruleset.css", includes=includes)

    def flatten_filter(model):
        return isinstance(model, cnxepub.CompositeDocument)

    def only_documents_filter(model):
        return isinstance(model, cnxepub.Document) \
               and not isinstance(model, cnxepub.CompositeDocument)

    for doc in cnxepub.flatten_to(binder, flatten_filter):
        publish_composite_model(cursor, doc, binder, publisher, message)

    for doc in cnxepub.flatten_to(binder, only_documents_filter):
        publish_collated_document(cursor, doc, binder)

    tree = cnxepub.model_to_tree(binder)
    publish_collated_tree(cursor, tree)

    return []
예제 #6
0
    def test(self, cursor):
        binder = use_cases.setup_COMPLEX_BOOK_ONE_in_archive(self, cursor)

        # Build some new metadata for the composite document.
        metadata = [x.metadata.copy()
                    for x in cnxepub.flatten_to_documents(binder)][0]
        del metadata['cnx-archive-uri']
        del metadata['version']
        metadata['title'] = "Made up of other things"

        publisher = [p['id'] for p in metadata['publishers']][0]
        message = "Composite addition"

        # Add some fake collation objects to the book.
        content = '<p class="para">composite</p>'
        composite_doc = cnxepub.CompositeDocument(None, content, metadata)

        from cnxpublishing.publish import publish_composite_model
        ident_hash = publish_composite_model(cursor, composite_doc, binder,
                                             publisher, message)

        # Shim the composite document into the binder.
        binder.append(composite_doc)

        tree = cnxepub.model_to_tree(binder)
        self.target(cursor, tree)

        cursor.execute("SELECT tree_to_json(%s, %s, TRUE)::json;",
                       (binder.id, binder.metadata['version'],))
        collated_tree = cursor.fetchone()[0]
        self.assertIn(composite_doc.ident_hash,
                      cnxepub.flatten_tree_to_ident_hashes(collated_tree))
예제 #7
0
    def test_baked(self):
        ident_hash = '[email protected]'
        binder = self.target(ident_hash, baked=True)

        # Briefly check for the existence of metadata.
        self.assertEqual(binder.metadata['title'], u'College Physics')

        # Check for containment

        expected_tree = {
            'id':
            u'[email protected]',
            'shortId':
            None,
            'title':
            u'College Physics',
            'contents': [{
                'id': u'209deb1f-1a46-4369-9e0d-18674cf58a3e@7',
                'shortId': u'IJ3rHxpG@7',
                'title': u'New Preface'
            }, {
                'id': u'174c4069-2743-42e9-adfe-4c7084f81fc5@1',
                'shortId': u'F0xAaSdD@1',
                'title': u'Other Composite'
            }],
        }

        self.assertEqual(cnxepub.model_to_tree(binder), expected_tree)
예제 #8
0
def publish_model(cursor, model, publisher, message):
    """Publishes the ``model`` and return its ident_hash."""
    publishers = publisher
    if isinstance(publishers, list) and len(publishers) > 1:
        raise ValueError("Only one publisher is allowed. '{}' "
                         "were given: {}" \
                         .format(len(publishers), publishers))
    module_ident, ident_hash = _insert_metadata(cursor, model,
                                                publisher, message)
    if isinstance(model, Document):
        file_arg = {
            'module_ident': module_ident,
            'filename': 'index.cnxml.html',
            'mime_type': 'text/html',
            'data': model.html.encode('utf-8'),
            }
        cursor.execute("""\
WITH file_insertion AS (
  INSERT INTO files (file) VALUES (%(data)s) RETURNING fileid)
INSERT INTO module_files
  (module_ident, fileid, filename, mimetype)
VALUES
  (%(module_ident)s,
   (SELECT fileid FROM file_insertion), 
   %(filename)s, %(mime_type)s)""", file_arg)
        for resource in model.resources:
            _insert_resource_file(cursor, module_ident, resource)
    elif isinstance(model, Binder):
        tree = cnxepub.model_to_tree(model)
        tree = _insert_tree(cursor, tree)
    return ident_hash
예제 #9
0
def bake(binder, recipe_id, publisher, message, cursor):
    """Given a `Binder` as `binder`, bake the contents and
    persist those changes alongside the published content.

    """
    recipe = _get_recipe(recipe_id, cursor)
    includes = _formatter_callback_factory()
    binder = collate_models(binder, ruleset=recipe, includes=includes)

    def flatten_filter(model):
        return (isinstance(model, cnxepub.CompositeDocument) or
                (isinstance(model, cnxepub.Binder) and
                 model.metadata.get('type') == 'composite-chapter'))

    def only_documents_filter(model):
        return isinstance(model, cnxepub.Document) \
            and not isinstance(model, cnxepub.CompositeDocument)

    for doc in cnxepub.flatten_to(binder, flatten_filter):
        publish_composite_model(cursor, doc, binder, publisher, message)

    for doc in cnxepub.flatten_to(binder, only_documents_filter):
        publish_collated_document(cursor, doc, binder)

    tree = cnxepub.model_to_tree(binder)
    amend_tree_with_slugs(tree)
    publish_collated_tree(cursor, tree)

    return []
예제 #10
0
def bake(binder, recipe_id, publisher, message, cursor):
    """Given a `Binder` as `binder`, bake the contents and
    persist those changes alongside the published content.

    """
    recipe = _get_recipe(recipe_id, cursor)
    includes = _formatter_callback_factory()
    binder = collate_models(binder, ruleset=recipe, includes=includes)

    def flatten_filter(model):
        return (isinstance(model, cnxepub.CompositeDocument) or
                (isinstance(model, cnxepub.Binder) and
                 model.metadata.get('type') == 'composite-chapter'))

    def only_documents_filter(model):
        return isinstance(model, cnxepub.Document) \
               and not isinstance(model, cnxepub.CompositeDocument)

    for doc in cnxepub.flatten_to(binder, flatten_filter):
        publish_composite_model(cursor, doc, binder, publisher, message)

    for doc in cnxepub.flatten_to(binder, only_documents_filter):
        publish_collated_document(cursor, doc, binder)

    tree = cnxepub.model_to_tree(binder)
    publish_collated_tree(cursor, tree)

    return []
예제 #11
0
    def test(self, cursor):
        binder = use_cases.setup_COMPLEX_BOOK_ONE_in_archive(self, cursor)

        # Build some new metadata for the composite document.
        metadata = [
            x.metadata.copy() for x in cnxepub.flatten_to_documents(binder)
        ][0]
        del metadata['cnx-archive-uri']
        del metadata['version']
        metadata['title'] = "Made up of other things"

        publisher = [p['id'] for p in metadata['publishers']][0]
        message = "Composite addition"

        # Add some fake collation objects to the book.
        content = '<p class="para">composite</p>'
        composite_doc = cnxepub.CompositeDocument(None, content, metadata)

        from cnxpublishing.publish import publish_composite_model
        ident_hash = publish_composite_model(cursor, composite_doc, binder,
                                             publisher, message)

        # Shim the composite document into the binder.
        binder.append(composite_doc)

        tree = cnxepub.model_to_tree(binder)
        self.target(cursor, tree)

        cursor.execute("SELECT tree_to_json(%s, %s, TRUE)::json;", (
            binder.id,
            binder.metadata['version'],
        ))
        collated_tree = cursor.fetchone()[0]
        self.assertIn(composite_doc.ident_hash,
                      cnxepub.flatten_tree_to_ident_hashes(collated_tree))
예제 #12
0
파일: main.py 프로젝트: Dimmus/cnx-epub
def main(argv=None):
    """Parse passed in cooked single HTML."""
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument('collated_html',
                        type=argparse.FileType('r'),
                        help='Path to the collated html'
                        ' file (use - for stdin)')
    parser.add_argument('-d',
                        '--dump-tree',
                        action='store_true',
                        help='Print out parsed model tree.')

    parser.add_argument('-o',
                        '--output',
                        type=argparse.FileType('w+'),
                        help='Write out epub of parsed tree.')

    parser.add_argument('-i',
                        '--input',
                        type=argparse.FileType('r'),
                        help='Read and copy resources/ for output epub.')

    args = parser.parse_args(argv)

    if args.input and args.output == sys.stdout:
        raise ValueError('Cannot output to stdout if reading resources')

    from cnxepub.collation import reconstitute
    binder = reconstitute(args.collated_html)

    if args.dump_tree:
        print(pformat(cnxepub.model_to_tree(binder)), file=sys.stdout)
    if args.output:
        cnxepub.adapters.make_epub(binder, args.output)

    if args.input:
        args.output.seek(0)
        zout = ZipFile(args.output, 'a', ZIP_DEFLATED)
        zin = ZipFile(args.input, 'r')
        for res in zin.namelist():
            if res.startswith('resources'):
                zres = zin.open(res)
                zi = zin.getinfo(res)
                zout.writestr(zi, zres.read(), ZIP_DEFLATED)
        zout.close()

    # TODO Check for documents that have no identifier.
    #      These should likely be composite-documents
    #      or the the metadata got wiped out.
    # docs = [x for x in cnxepub.flatten_to(binder, only_documents_filter)
    #         if x.ident_hash is None]

    return 0
예제 #13
0
파일: main.py 프로젝트: Connexions/cnx-epub
def main(argv=None):
    """Parse passed in cooked single HTML."""
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument('collated_html', type=argparse.FileType('r'),
                        help='Path to the collated html'
                             ' file (use - for stdin)')
    parser.add_argument('-d', '--dump-tree', action='store_true',
                        help='Print out parsed model tree.')

    parser.add_argument('-o', '--output', type=argparse.FileType('w+'),
                        help='Write out epub of parsed tree.')

    parser.add_argument('-i', '--input', type=argparse.FileType('r'),
                        help='Read and copy resources/ for output epub.')

    args = parser.parse_args(argv)

    if args.input and args.output == sys.stdout:
        raise ValueError('Cannot output to stdout if reading resources')

    from cnxepub.collation import reconstitute
    binder = reconstitute(args.collated_html)

    if args.dump_tree:
        print(pformat(cnxepub.model_to_tree(binder)),
              file=sys.stdout)
    if args.output:
        cnxepub.adapters.make_epub(binder, args.output)

    if args.input:
        args.output.seek(0)
        zout = ZipFile(args.output, 'a', ZIP_DEFLATED)
        zin = ZipFile(args.input, 'r')
        for res in zin.namelist():
            if res.startswith('resources'):
                zres = zin.open(res)
                zi = zin.getinfo(res)
                zout.writestr(zi, zres.read(), ZIP_DEFLATED)
        zout.close()

    # TODO Check for documents that have no identifier.
    #      These should likely be composite-documents
    #      or the the metadata got wiped out.
    # docs = [x for x in cnxepub.flatten_to(binder, only_documents_filter)
    #         if x.ident_hash is None]

    return 0
예제 #14
0
def publish_model(cursor, model, publisher, message):
    """Publishes the ``model`` and return its ident_hash."""
    publishers = publisher
    if isinstance(publishers, list) and len(publishers) > 1:
        raise ValueError("Only one publisher is allowed. '{}' "
                         "were given: {}".format(len(publishers), publishers))
    module_ident, ident_hash = _insert_metadata(cursor, model, publisher,
                                                message)

    for resource in getattr(model, 'resources', []):
        _insert_resource_file(cursor, module_ident, resource)

    if isinstance(model, Document):
        html = str(cnxepub.DocumentContentFormatter(model)).encode('utf-8')
        sha1 = hashlib.new('sha1', html).hexdigest()
        cursor.execute("SELECT fileid FROM files WHERE sha1 = %s", (sha1, ))
        try:
            fileid = cursor.fetchone()[0]
        except TypeError:
            file_args = {
                'media_type': 'text/html',
                'data': psycopg2.Binary(html),
            }
            cursor.execute(
                """\
            insert into files (file, media_type)
            VALUES (%(data)s, %(media_type)s)
            returning fileid""", file_args)
            fileid = cursor.fetchone()[0]
        args = {
            'module_ident': module_ident,
            'filename': 'index.cnxml.html',
            'fileid': fileid,
        }
        cursor.execute(
            """\
        INSERT INTO module_files
          (module_ident, fileid, filename)
        VALUES
          (%(module_ident)s, %(fileid)s, %(filename)s)""", args)

    elif isinstance(model, Binder):
        tree = cnxepub.model_to_tree(model)
        tree = _insert_tree(cursor, tree)
    return ident_hash
예제 #15
0
def publish_model(cursor, model, publisher, message):
    """Publishes the ``model`` and return its ident_hash."""
    publishers = publisher
    if isinstance(publishers, list) and len(publishers) > 1:
        raise ValueError("Only one publisher is allowed. '{}' "
                         "were given: {}"
                         .format(len(publishers), publishers))
    module_ident, ident_hash = _insert_metadata(cursor, model,
                                                publisher, message)

    for resource in getattr(model, 'resources', []):
        _insert_resource_file(cursor, module_ident, resource)

    if isinstance(model, Document):
        html = bytes(cnxepub.DocumentContentFormatter(model))
        sha1 = hashlib.new('sha1', html).hexdigest()
        cursor.execute("SELECT fileid FROM files WHERE sha1 = %s", (sha1,))
        try:
            fileid = cursor.fetchone()[0]
        except TypeError:
            file_args = {
                'media_type': 'text/html',
                'data': psycopg2.Binary(html),
            }
            cursor.execute("""\
            insert into files (file, media_type)
            VALUES (%(data)s, %(media_type)s)
            returning fileid""", file_args)
            fileid = cursor.fetchone()[0]
        args = {
            'module_ident': module_ident,
            'filename': 'index.cnxml.html',
            'fileid': fileid,
        }
        cursor.execute("""\
        INSERT INTO module_files
          (module_ident, fileid, filename)
        VALUES
          (%(module_ident)s, %(fileid)s, %(filename)s)""", args)

    elif isinstance(model, Binder):
        tree = cnxepub.model_to_tree(model)
        tree = _insert_tree(cursor, tree)
    return ident_hash
예제 #16
0
    def test_baked(self):
        ident_hash = '[email protected]'
        binder = self.target(ident_hash, baked=True)

        # Briefly check for the existence of metadata.
        self.assertEqual(binder.metadata['title'], u'College Physics')

        # Check for containment

        expected_tree = {
            'id': u'[email protected]',
            'shortId': '[email protected]',
            'title': u'College Physics',
            'contents': [
                {'id': u'209deb1f-1a46-4369-9e0d-18674cf58a3e@7',
                 'shortId': u'IJ3rHxpG@7',
                 'title': u'New Preface'},
                {'id': u'174c4069-2743-42e9-adfe-4c7084f81fc5@1',
                 'shortId': u'F0xAaSdD@1',
                 'title': u'Other Composite'}
                ],
            }

        self.assertEqual(cnxepub.model_to_tree(binder), expected_tree)
예제 #17
0
    def test_assembly(self):
        ident_hash = '[email protected]'
        binder = self.target(ident_hash)

        # Briefly check for the existence of metadata.
        self.assertEqual(binder.metadata['title'], u'College Physics')

        # Check for containment
        expected_tree = {
            'shortId':
            None,
            'id':
            u'[email protected]',
            'title':
            u'College Physics',
            'contents': [{
                'shortId': u'IJ3rHxpG@7',
                'id': u'209deb1f-1a46-4369-9e0d-18674cf58a3e@7',
                'title': u'Preface'
            }, {
                'shortId':
                u'subcol',
                'id':
                u'subcol',
                'title':
                u'Introduction: The Nature of Science and Physics',
                'contents': [{
                    'shortId':
                    u'88mrcKkW@3',
                    'id':
                    u'f3c9ab70-a916-4d8c-9256-42953287b4e9@3',
                    'title':
                    u'Introduction to Science and the Realm of Physics, Physical Quantities, and Units'
                }, {
                    'shortId': u'05W1Zl_j@4',
                    'id': u'd395b566-5fe3-4428-bcb2-19016e3aa3ce@4',
                    'title': u'Physics: An Introduction'
                }, {
                    'shortId': u'yL26vGKx@6',
                    'id': u'c8bdbabc-62b1-4a5f-b291-982ab25756d7@6',
                    'title': u'Physical Quantities and Units'
                }, {
                    'shortId':
                    u'UVLOqIKa@7',
                    'id':
                    u'5152cea8-829a-4aaf-bcc5-c58a416ecb66@7',
                    'title':
                    u'Accuracy, Precision, and Significant Figures'
                }, {
                    'shortId': u'WDixBUHN@5',
                    'id': u'5838b105-41cd-4c3d-a957-3ac004a48af3@5',
                    'title': u'Approximation'
                }]
            }, {
                'shortId':
                u'subcol',
                'id':
                u'subcol',
                'title':
                u"Further Applications of Newton's Laws: Friction, Drag, and Elasticity",
                'contents': [{
                    'shortId':
                    u'JKLtEyKm@2',
                    'id':
                    u'24a2ed13-22a6-47d6-97a3-c8aa8d54ac6d@2',
                    'title':
                    u'Introduction: Further Applications of Newton\u2019s Laws'
                }, {
                    'shortId': u'6icTBvfy@5',
                    'id': u'ea271306-f7f2-46ac-b2ec-1d80ff186a59@5',
                    'title': u'Friction'
                }, {
                    'shortId': u'JjRqQoS5@6',
                    'id': u'26346a42-84b9-48ad-9f6a-62303c16ad41@6',
                    'title': u'Drag Forces'
                }, {
                    'shortId': u'VvHFwUAU@8',
                    'id': u'56f1c5c1-4014-450d-a477-2121e276beca@8',
                    'title': u'Elasticity: Stress and Strain'
                }],
            }, {
                'shortId': u'9gJNihho@3',
                'id': u'f6024d8a-1868-44c7-ab65-45419ef54881@3',
                'title': u'Atomic Masses'
            }, {
                'shortId': u'clA4axSn@2',
                'id': u'7250386b-14a7-41a2-b8bf-9e9ab872f0dc@2',
                'title': u'Selected Radioactive Isotopes'
            }, {
                'shortId': u'wKdmWcMR@5',
                'id': u'c0a76659-c311-405f-9a99-15c71af39325@5',
                'title': u'Useful Inf\xf8rmation'
            }, {
                'shortId': u'rj4Y3mON@5',
                'id': u'ae3e18de-638d-4738-b804-dc69cd4db3a3@5',
                'title': u'Glossary of Key Symbols and Notation'
            }],
        }

        self.maxDiff = None
        self.assertEqual(cnxepub.model_to_tree(binder), expected_tree)

        # Check translucent binder metadata
        translucent_binder = binder[1]
        self.assertTrue(
            isinstance(translucent_binder, cnxepub.TranslucentBinder))
        expected_metadata = {
            'id': u'subcol',
            'shortId': 'subcol',
            'title': 'Introduction: The Nature of Science and Physics',
        }
        self.assertEqual(expected_metadata, translucent_binder.metadata)
예제 #18
0
    def test_assembly(self):
        ident_hash = '[email protected]'
        binder = self.target(ident_hash)

        # Briefly check for the existence of metadata.
        self.assertEqual(binder.metadata['title'], u'College Physics')

        # Check for containment
        expected_tree = {
            'shortId': '[email protected]',
            'id': u'[email protected]',
            'title': u'College Physics',
            'contents': [
                {'shortId': u'IJ3rHxpG@7',
                 'id': u'209deb1f-1a46-4369-9e0d-18674cf58a3e@7',
                 'title': u'Preface'},
                {
                 'id': u'[email protected]',
                 'shortId': u'[email protected]',
                 'title': u'Introduction: The Nature of Science and Physics',
                 'contents': [
                    {'shortId': u'88mrcKkW@3',
                     'id': u'f3c9ab70-a916-4d8c-9256-42953287b4e9@3',
                     'title': u'Introduction to Science and the Realm of Physics, Physical Quantities, and Units'},
                    {'shortId': u'05W1Zl_j@4',
                     'id': u'd395b566-5fe3-4428-bcb2-19016e3aa3ce@4',
                     'title': u'Physics: An Introduction'},
                    {'shortId': u'yL26vGKx@6',
                     'id': u'c8bdbabc-62b1-4a5f-b291-982ab25756d7@6',
                     'title': u'Physical Quantities and Units'},
                    {'shortId': u'UVLOqIKa@7',
                     'id': u'5152cea8-829a-4aaf-bcc5-c58a416ecb66@7',
                     'title': u'Accuracy, Precision, and Significant Figures'},
                    {'shortId': u'WDixBUHN@5',
                     'id': u'5838b105-41cd-4c3d-a957-3ac004a48af3@5',
                     'title': u'Approximation'}]},
                    {'shortId': u'[email protected]',
                     'id': u'[email protected]',
                     'title': u"Further Applications of Newton's Laws: Friction, Drag, and Elasticity",
                     'contents': [
                        {'shortId': u'JKLtEyKm@2',
                         'id': u'24a2ed13-22a6-47d6-97a3-c8aa8d54ac6d@2',
                         'title': u'Introduction: Further Applications of Newton\u2019s Laws'},
                        {'shortId': u'6icTBvfy@5',
                         'id': u'ea271306-f7f2-46ac-b2ec-1d80ff186a59@5',
                         'title': u'Friction'},
                        {'shortId': u'JjRqQoS5@6',
                         'id': u'26346a42-84b9-48ad-9f6a-62303c16ad41@6',
                         'title': u'Drag Forces'},
                        {'shortId': u'VvHFwUAU@8',
                         'id': u'56f1c5c1-4014-450d-a477-2121e276beca@8',
                         'title': u'Elasticity: Stress and Strain'}], },
                {'shortId': u'9gJNihho@3',
                 'id': u'f6024d8a-1868-44c7-ab65-45419ef54881@3',
                 'title': u'Atomic Masses'},
                {'shortId': u'clA4axSn@2',
                 'id': u'7250386b-14a7-41a2-b8bf-9e9ab872f0dc@2',
                 'title': u'Selected Radioactive Isotopes'},
                {'shortId': u'wKdmWcMR@5',
                 'id': u'c0a76659-c311-405f-9a99-15c71af39325@5',
                 'title': u'Useful Inf\xf8rmation'},
                {'shortId': u'rj4Y3mON@5',
                 'id': u'ae3e18de-638d-4738-b804-dc69cd4db3a3@5',
                 'title': u'Glossary of Key Symbols and Notation'}],
        }

        self.maxDiff = None
        self.assertEqual(cnxepub.model_to_tree(binder), expected_tree)

        # Check translucent binder metadata
        translucent_binder = binder[1]
        self.assertTrue(
            isinstance(translucent_binder, cnxepub.TranslucentBinder))
        expected_metadata = {
            'id': u'[email protected]',
            'shortId': u'[email protected]',
            'title': 'Introduction: The Nature of Science and Physics',
            }
        for k, v in expected_metadata.items():
            self.assertEqual(translucent_binder.metadata[k], v)