diff --git a/docs/dev/analysis/features/header.rst b/docs/dev/analysis/features/header.rst index 5eac6f5de..6df2560aa 100644 --- a/docs/dev/analysis/features/header.rst +++ b/docs/dev/analysis/features/header.rst @@ -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 @@ -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:: @@ -174,6 +190,23 @@ Distinct first, even, and odd page headers:: ... +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`:: + + + + + + + + Foobar + + + Word Behavior ------------- diff --git a/docx/__init__.py b/docx/__init__.py index 7dadb58e7..e8d8108df 100644 --- a/docx/__init__.py +++ b/docx/__init__.py @@ -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 @@ -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, diff --git a/docx/header.py b/docx/header.py index ed5e69cee..adba560d7 100644 --- a/docx/header.py +++ b/docx/header.py @@ -8,6 +8,7 @@ absolute_import, division, print_function, unicode_literals ) +from .blkcntnr import BlockItemContainer from .shared import ElementProxy, lazyproperty @@ -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. diff --git a/docx/parts/document.py b/docx/parts/document.py index 87e9ee2ef..20e6d2342 100644 --- a/docx/parts/document.py +++ b/docx/parts/document.py @@ -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] def save(self, path_or_stream): """ diff --git a/docx/parts/header.py b/docx/parts/header.py new file mode 100644 index 000000000..f59c8ba54 --- /dev/null +++ b/docx/parts/header.py @@ -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) diff --git a/features/steps/header.py b/features/steps/header.py index 21e6ba756..afe96f6f9 100644 --- a/features/steps/header.py +++ b/features/steps/header.py @@ -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}') diff --git a/tests/test_header.py b/tests/test_header.py index 0ee5429ce..ead7594c7 100644 --- a/tests/test_header.py +++ b/tests/test_header.py @@ -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): @@ -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), @@ -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(