Skip to content

Header and Footer #291

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 34 additions & 1 deletion docs/dev/analysis/features/header.rst
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,16 @@ Conversely, an existing header is deleted from a section by assigning True to
>>> header.is_linked_to_previous
True

The methods `add_paragraph` and `paragraphs` allow you to add or change the
contents of a header, just like on a document instance::

>>> p1 = header.add_paragraph('foo')
>>> p2 = header.add_paragraph('bar')
>>> len(header.paragraphs)
2
>>> header.paragraphs[0].text
'foo'

The document settings object has a read/write
`.odd_and_even_pages_header_footer` property that indicates verso and recto
pages will have a different header. An existing even page header definition is
Expand Down Expand Up @@ -126,9 +136,15 @@ automatically create a new first page header definition::
Specimen XML
------------

headerReference
~~~~~~~~~~~~~~~

.. highlight:: xml

There are seven different permutations of headers:
The `headerReference` controls where and how the header content displays in the
document.

There are seven different ways the same header content could appear:

The same header on all pages of the document::

Expand Down Expand Up @@ -174,6 +190,23 @@ Distinct first, even, and odd page headers::
...
</w:sectPr>

hdr
~~~

All the actual header content is contained in an xml file separate from the
main `document.xml`. The header file name is arbitrary but it's usually
something like `header1.xml`::

<w:hdr>
<w:p>
<w:pPr>
<w:pStyle w:val="Header"/>
</w:pPr>
<w:r>
<w:t>Foobar</w:t>
</w:r>
</w:p>
</w:hdr>

Word Behavior
-------------
Expand Down
2 changes: 2 additions & 0 deletions docx/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from docx.opc.parts.coreprops import CorePropertiesPart

from docx.parts.document import DocumentPart
from docx.parts.header import HeaderPart
from docx.parts.image import ImagePart
from docx.parts.numbering import NumberingPart
from docx.parts.settings import SettingsPart
Expand All @@ -30,6 +31,7 @@ def part_class_selector(content_type, reltype):
PartFactory.part_type_for[CT.WML_NUMBERING] = NumberingPart
PartFactory.part_type_for[CT.WML_SETTINGS] = SettingsPart
PartFactory.part_type_for[CT.WML_STYLES] = StylesPart
PartFactory.part_type_for[CT.WML_HEADER] = HeaderPart

del (
CT, CorePropertiesPart, DocumentPart, NumberingPart, PartFactory,
Expand Down
3 changes: 2 additions & 1 deletion docx/header.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
absolute_import, division, print_function, unicode_literals
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@scanny ok here's my first stab at a unit test. I modeled it after this commit:

bddd27a

The first acceptance test passes now, as does this unittest!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is great @eupharis :)

I'll have a look this evening and move this along :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@scanny ok i think this commit is good to go. unittest and acceptance test both pass.

I went with ElementProxy instead of object for Header's base class just so that I could pass sectPr without writing some custom init method.

Class methods should be in alphabetical order. I didn't worry about tests though. It seems like those aren't alphabetical currently.

lazyproperty is cool! I have done that a bunch but I never thought to abstract it out like that. Super handy.

What's next? The end goal right now is "Able to load a document with header text and access that text" right?

Lots of steps to get there.

)

from .blkcntnr import BlockItemContainer
from .shared import ElementProxy, lazyproperty


Expand Down Expand Up @@ -51,7 +52,7 @@ class Header(_BaseHeaderFooter):
"""


class HeaderFooterBody(object):
class HeaderFooterBody(BlockItemContainer):
"""
The rich-text body of a header or footer. Supports the same rich text
operations as a document, such as paragraphs and tables.
Expand Down
10 changes: 9 additions & 1 deletion docx/parts/document.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,15 @@ def related_hdrftr_body(self, rId):
Return the |HeaderFooterBody| object corresponding to the related
part identified by *rId*.
"""
raise NotImplementedError
part = self.get_related_part(rId)
return part.body

def get_related_part(self, rId):
""" HACK this isn't strictly necessary
just adding it because it seems much easier to mock than
self.rels.related_parts
"""
return self.rels.related_parts[rId]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's keep this all in one piece. Here's an example of how I mocked related_parts in python-pptx. .related_parts is available directly on the part object, no need to go out to .rels to get it :)

    # in pptx.parts.presentation ------
    def related_slide(self, rId):
        """
        Return the |Slide| object for the related |SlidePart| corresponding
        to relationship key *rId*.
        """
        return self.related_parts[rId].slide

    # the rest of this is in tests.parts.test_presentation ------
    def it_provides_access_to_a_related_slide(self, slide_fixture):
        prs_part, rId, slide_ = slide_fixture
        slide = prs_part.related_slide(rId)
        prs_part.related_parts.__getitem__.assert_called_once_with(rId)
        assert slide is slide_

    @pytest.fixture
    def slide_fixture(self, slide_, related_parts_prop_):
        prs_part = PresentationPart(None, None, None, None)
        rId = 'rId42'
        related_parts_ = related_parts_prop_.return_value
        related_parts_.__getitem__.return_value.slide = slide_
        return prs_part, rId, slide_

    @pytest.fixture
    def related_parts_prop_(self, request):
        return property_mock(request, PresentationPart, 'related_parts')


def save(self, path_or_stream):
"""
Expand Down
24 changes: 24 additions & 0 deletions docx/parts/header.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# encoding: utf-8

"""
|HeaderPart| and closely related objects
"""

from __future__ import (
absolute_import, division, print_function, unicode_literals
)

from ..header import HeaderFooterBody
from ..opc.part import XmlPart


class HeaderPart(XmlPart):
@property
def body(self):
"""
A |HeaderFooterBody| proxy object for the `w:hdr` element in this part,
"""
# TODO write CT_HeaderFooter
# element = CT_HeaderFooter(self.element)
# how to access parent here? is it necessary?
return HeaderFooterBody(self.element, None)
2 changes: 1 addition & 1 deletion features/steps/header.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ def then_header_body_contains_the_text_of_the_header(context):
@then('header.body is a BlockItemContainer object')
def then_header_body_is_a_BlockItemContainer_object(context):
header = context.header
assert type(header.body).__name__ == 'BlockItemContainer'
assert type(header.body).__name__ == 'HeaderFooterBody'


@then('header.is_linked_to_previous is {value}')
Expand Down
27 changes: 26 additions & 1 deletion tests/test_header.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,10 @@
from docx.enum.header import WD_HEADER_FOOTER
from docx.header import _BaseHeaderFooter, Header, HeaderFooterBody
from docx.parts.document import DocumentPart
from docx.parts.header import HeaderPart

from .unitutil.cxml import element
from .unitutil.mock import call, instance_mock, property_mock
from .unitutil.mock import call, instance_mock, method_mock, property_mock


class Describe_BaseHeaderFooter(object):
Expand All @@ -30,8 +31,28 @@ def it_provides_access_to_its_body(self, body_fixture):
assert header.part.related_hdrftr_body.call_args_list == calls
assert body == expected_value

def it_provides_access_to_the_related_hdrftr_body(self, hdrftr_fixture):
document_part, get_related_parts, header_part_ = hdrftr_fixture
rId = 'rId1'
body = document_part.related_hdrftr_body(rId)
get_related_parts.assert_called_once_with(rId)
assert body == header_part_.body

# fixtures -------------------------------------------------------

@pytest.fixture
def hdrftr_fixture(self, request, header_part_, body_):
header_part_.body = body_
document_part = DocumentPart(None, None, None, None)

get_related_part = method_mock(
request,
DocumentPart,
'get_related_part')
get_related_part.return_value = header_part_

return document_part, get_related_part, header_part_

@pytest.fixture(params=[
('w:sectPr', None),
('w:sectPr/w:headerReference{w:type=even,r:id=rId6}', None),
Expand Down Expand Up @@ -64,6 +85,10 @@ def body_(self, request):
def document_part_(self, request):
return instance_mock(request, DocumentPart)

@pytest.fixture
def header_part_(self, request):
return instance_mock(request, HeaderPart)

@pytest.fixture
def part_prop_(self, request, document_part_):
return property_mock(
Expand Down